trainerbase 4.8.1__tar.gz → 4.9.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 (40) hide show
  1. {trainerbase-4.8.1 → trainerbase-4.9.0}/PKG-INFO +3 -3
  2. trainerbase-4.9.0/pyproject.toml +92 -0
  3. trainerbase-4.8.1/pyproject.toml → trainerbase-4.9.0/pyproject.toml.orig +27 -20
  4. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/codeinjection.py +82 -60
  5. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/common/helpers.py +14 -7
  6. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/common/rainbow.py +1 -2
  7. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/config.py +1 -1
  8. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gameobject.py +3 -3
  9. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/helpers.py +1 -1
  10. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/misc.py +1 -1
  11. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/objects.py +7 -5
  12. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/speedhack.py +1 -1
  13. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/tapper.py +2 -2
  14. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/teleport.py +2 -2
  15. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/logger.py +1 -1
  16. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/main.py +1 -2
  17. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/memory.py +91 -15
  18. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/process.py +4 -2
  19. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/scriptengine.py +11 -6
  20. {trainerbase-4.8.1 → trainerbase-4.9.0}/README.md +0 -0
  21. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/__init__.py +0 -0
  22. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/abc.py +0 -0
  23. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/common/__init__.py +0 -0
  24. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/common/abc.py +0 -0
  25. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/common/keyboard.py +0 -0
  26. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/common/tapper.py +0 -0
  27. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/common/teleport.py +0 -0
  28. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/__init__.py +0 -0
  29. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/injections.py +0 -0
  30. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/rainbow.py +0 -0
  31. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/scripts.py +0 -0
  32. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/types.py +0 -0
  33. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/py.typed +0 -0
  34. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/speedhack.py +0 -0
  35. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/tts.py +0 -0
  36. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/vendor/FASM.EXE +0 -0
  37. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/vendor/big_logo.png +0 -0
  38. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/vendor/speedhack32.dll +0 -0
  39. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/vendor/speedhack64.dll +0 -0
  40. {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/vendor/trainerbase.ico +0 -0
@@ -1,17 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trainerbase
3
- Version: 4.8.1
3
+ Version: 4.9.0
4
4
  Summary: Framework for creating game trainers/mods
5
5
  Author: v01d, doomayka
6
6
  Author-email: v01d <v01d@v01d.ru>, doomayka <doomaykaka@gmail.com>
7
7
  License-Expression: MIT
8
8
  Classifier: Programming Language :: Python :: Implementation :: CPython
9
9
  Classifier: Programming Language :: Python :: 3.14
10
- Requires-Dist: dearpygui>=2.2
10
+ Requires-Dist: dearpygui>=2.3.1
11
11
  Requires-Dist: keyboard>=0.13.5
12
12
  Requires-Dist: mouse>=0.7.1
13
13
  Requires-Dist: psutil>=7.2.2
14
- Requires-Dist: pydantic>=2.12.5
14
+ Requires-Dist: pydantic>=2.14.0
15
15
  Requires-Dist: pymem>=1.14.0
16
16
  Requires-Dist: pyttsx3>=2.99
17
17
  Requires-Dist: trainerbase-injector>=1.0.1
@@ -0,0 +1,92 @@
1
+ [project]
2
+ name = "trainerbase"
3
+ version = "4.9.0"
4
+ description = "Framework for creating game trainers/mods"
5
+ license = "MIT"
6
+ requires-python = ">=3.14"
7
+ readme = "README.md"
8
+ dependencies = [
9
+ "dearpygui>=2.3.1",
10
+ "keyboard>=0.13.5",
11
+ "mouse>=0.7.1",
12
+ "psutil>=7.2.2",
13
+ "pydantic>=2.14.0",
14
+ "pymem>=1.14.0",
15
+ "pyttsx3>=2.99",
16
+ "trainerbase-injector>=1.0.1",
17
+ ]
18
+ classifiers = [
19
+ "Programming Language :: Python :: Implementation :: CPython",
20
+ "Programming Language :: Python :: 3.14",
21
+ ]
22
+
23
+ [[project.authors]]
24
+ name = "v01d"
25
+ email = "v01d@v01d.ru"
26
+
27
+ [[project.authors]]
28
+ name = "doomayka"
29
+ email = "doomaykaka@gmail.com"
30
+
31
+ [project.urls]
32
+ Homepage = "https://gitlab.com/python-trainers/"
33
+ Repository = "https://gitlab.com/python-trainers/trainer-base"
34
+
35
+ [tool.ruff]
36
+ src = ["trainerbase"]
37
+ line-length = 120
38
+
39
+ [tool.ruff.lint]
40
+ select = ["ALL"]
41
+ ignore = [
42
+ "D",
43
+ "ANN",
44
+ "TC",
45
+ "EM",
46
+ "A005",
47
+ "FIX002",
48
+ "COM812",
49
+ "TC002",
50
+ "S607",
51
+ "BLE001",
52
+ "TRY003",
53
+ "RET504",
54
+ "PLR2004",
55
+ "PLR0913",
56
+ "PLW2901",
57
+ "TD002",
58
+ "TD003",
59
+ "CPY001",
60
+ "PLR0917",
61
+ ]
62
+
63
+ [tool.ruff.lint.isort]
64
+ lines-after-imports = 2
65
+
66
+ [tool.ruff.format]
67
+ line-ending = "lf"
68
+
69
+ [tool.pyright]
70
+ include = ["trainerbase"]
71
+ typeCheckingMode = "standard"
72
+ pythonPlatform = "All"
73
+ pythonVersion = "3.14"
74
+
75
+ [[tool.pyright.executionEnvironments]]
76
+ root = "."
77
+ extraPaths = ["src"]
78
+
79
+ [tool.uv.build-backend]
80
+ module-name = "trainerbase"
81
+ module-root = ""
82
+
83
+ [build-system]
84
+ requires = ["uv_build>=0.11.3,<1.0.0"]
85
+ build-backend = "uv_build"
86
+
87
+ [dependency-groups]
88
+ dev = [
89
+ "invoke>=3.0.3",
90
+ "pyright>=1.1.414",
91
+ "ruff>=0.17.0",
92
+ ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "trainerbase"
3
- version = "4.8.1"
3
+ version = "4.9.0"
4
4
  description = "Framework for creating game trainers/mods"
5
5
  license = "MIT"
6
6
  authors = [
@@ -10,11 +10,11 @@ authors = [
10
10
  requires-python = ">=3.14"
11
11
  readme = "README.md"
12
12
  dependencies = [
13
- "dearpygui>=2.2",
13
+ "dearpygui>=2.3.1",
14
14
  "keyboard>=0.13.5",
15
15
  "mouse>=0.7.1",
16
16
  "psutil>=7.2.2",
17
- "pydantic>=2.12.5",
17
+ "pydantic>=2.14.0",
18
18
  "pymem>=1.14.0",
19
19
  "pyttsx3>=2.99",
20
20
  "trainerbase-injector>=1.0.1",
@@ -34,20 +34,27 @@ src = ["trainerbase"]
34
34
  line-length = 120
35
35
 
36
36
  [tool.ruff.lint]
37
- select = [
38
- "E", # pycodestyle
39
- "F", # Pyflakes
40
- "UP", # pyupgrade
41
- "B", # flake8-bugbear
42
- "SIM", # flake8-simplify
43
- "I", # isort
44
- "N", # pep8-naming
45
- "FIX",
46
- "FBT",
47
- "ARG",
48
- "PTH",
49
- "ERA",
50
- "RUF",
37
+ select = ["ALL"]
38
+ ignore = [
39
+ "D",
40
+ "ANN",
41
+ "TC",
42
+ "EM",
43
+ "A005",
44
+ "FIX002",
45
+ "COM812",
46
+ "TC002",
47
+ "S607",
48
+ "BLE001",
49
+ "TRY003",
50
+ "RET504",
51
+ "PLR2004",
52
+ "PLR0913",
53
+ "PLW2901",
54
+ "TD002",
55
+ "TD003",
56
+ "CPY001",
57
+ "PLR0917",
51
58
  ]
52
59
 
53
60
  [tool.ruff.lint.isort]
@@ -75,7 +82,7 @@ build-backend = "uv_build"
75
82
 
76
83
  [dependency-groups]
77
84
  dev = [
78
- "invoke>=2.2.1",
79
- "pyright>=1.1.407",
80
- "ruff>=0.15.9",
85
+ "invoke>=3.0.3",
86
+ "pyright>=1.1.414",
87
+ "ruff>=0.17.0",
81
88
  ]
@@ -11,7 +11,15 @@ from pymem.exception import MemoryWriteError
11
11
 
12
12
  from trainerbase.config import config
13
13
  from trainerbase.logger import logger
14
- from trainerbase.memory import ARCH, ConvertibleToAddress, allocate, ensure_address
14
+ from trainerbase.memory import (
15
+ ARCH,
16
+ REL32_JUMP_SIZE,
17
+ REL32_MAX_DISPLACEMENT,
18
+ REL32_MIN_DISPLACEMENT,
19
+ ConvertibleToAddress,
20
+ allocate,
21
+ ensure_address,
22
+ )
15
23
  from trainerbase.process import pm
16
24
 
17
25
 
@@ -19,6 +27,7 @@ FASM_EXECUTABLE_PATH: Final[Path] = config.vendor.folder / "FASM.EXE"
19
27
  ORIGINAL_CODE_PLACEHOLDER: Final[str] = "__original_code__"
20
28
  BASE_ADDRESS_PLACEHOLDER: Final[str] = "__base_address__"
21
29
  MOVE_X64_MACROS: Final[str] = "__move_x64__"
30
+ LONG_X64_JUMP_SIZE: Final[int] = 14
22
31
 
23
32
 
24
33
  type Code = bytes | str
@@ -56,10 +65,13 @@ class CodeInjection(AbstractCodeInjection):
56
65
 
57
66
  @override
58
67
  def inject(self):
68
+ if self._injected:
69
+ return
70
+
59
71
  self.injection_point = self.address.resolve()
60
72
  self.original_code = pm.read_bytes(self.injection_point, len(self.code_to_inject))
61
73
 
62
- logger.debug(f"Injecting {len(self.code_to_inject)} byte(s) at 0x{self.injection_point:X}")
74
+ logger.debug("Injecting %d byte(s) at 0x%X", len(self.code_to_inject), self.injection_point)
63
75
 
64
76
  pm.write_bytes(self.injection_point, self.code_to_inject, len(self.code_to_inject))
65
77
  self._injected = True
@@ -67,7 +79,7 @@ class CodeInjection(AbstractCodeInjection):
67
79
  @override
68
80
  def eject(self):
69
81
  if self._injected:
70
- logger.debug(f"Ejecting {len(self.code_to_inject)} byte(s) from 0x{self.injection_point:X}")
82
+ logger.debug("Ejecting %d byte(s) from 0x%X", len(self.code_to_inject), self.injection_point)
71
83
 
72
84
  pm.write_bytes(self.injection_point, self.original_code, len(self.original_code))
73
85
  self._injected = False
@@ -82,12 +94,9 @@ class AllocatingCodeInjection(AbstractCodeInjection):
82
94
  code_to_inject: Code,
83
95
  *,
84
96
  original_code_length: int = 0,
85
- new_memory_size: int = 1024,
86
- is_long_x64_jump_needed: bool | None = None,
97
+ new_memory_size: int | None = None,
98
+ is_long_x64_jump_needed: bool = False,
87
99
  ):
88
- if is_long_x64_jump_needed is None:
89
- is_long_x64_jump_needed = pm.is_64_bit
90
-
91
100
  self.is_long_x64_jump_needed = is_long_x64_jump_needed
92
101
  self.address = ensure_address(address)
93
102
  self.code_to_inject = code_to_inject
@@ -101,58 +110,69 @@ class AllocatingCodeInjection(AbstractCodeInjection):
101
110
 
102
111
  @override
103
112
  def inject(self):
104
- self.injection_point = self.address.resolve()
105
- self.new_memory_address: int = allocate(self.new_memory_size)
106
- self.original_code = pm.read_bytes(self.injection_point, self.original_code_length)
113
+ if self.new_memory_address:
114
+ return
107
115
 
116
+ injection_point = self.address.resolve()
117
+ original_code = pm.read_bytes(injection_point, self.original_code_length)
108
118
  code_to_inject = self.code_to_inject
109
119
 
110
120
  if isinstance(code_to_inject, str):
111
121
  code_to_inject = preprocess_asm(
112
122
  code_to_inject,
113
- original_code=self.original_code,
123
+ original_code=original_code,
114
124
  base_address=pm.base_address,
115
125
  )
116
126
 
117
- if isinstance(code_to_inject, bytearray):
118
- raise TypeError("Code to inject must not be bytearray")
119
-
120
127
  code_to_inject = compile_asm(code_to_inject)
128
+ jumper_length = LONG_X64_JUMP_SIZE if self.is_long_x64_jump_needed else REL32_JUMP_SIZE
129
+ if jumper_length > self.original_code_length:
130
+ raise ValueError(f"Jumper length > original code length: {jumper_length} > {self.original_code_length}")
121
131
 
122
- if self.is_long_x64_jump_needed:
123
- code_to_inject = compile_asm("pop rax") + code_to_inject + compile_asm("push rax")
124
- jump_to_new_mem = compile_asm(get_long_x64_jump_to_new_mem_code(self.new_memory_address))
125
- jumper_length = len(jump_to_new_mem)
126
- jump_back_address = self.injection_point + jumper_length - 1
127
- jump_back = compile_asm(get_long_x64_jump_back_code(jump_back_address))
128
- else:
129
- jump_to_new_mem = compile_asm(get_simple_jump_code(self.new_memory_address, self.injection_point))
130
- jumper_length = len(jump_to_new_mem)
131
- jump_back_address = self.injection_point + jumper_length
132
- jump_back = compile_asm(
133
- get_simple_jump_code(jump_back_address, self.new_memory_address + len(code_to_inject)),
132
+ jump_back_address = injection_point + self.original_code_length
133
+ required_size = len(code_to_inject) + (LONG_X64_JUMP_SIZE if pm.is_64_bit else REL32_JUMP_SIZE)
134
+ memory_size = required_size if self.new_memory_size is None else self.new_memory_size
135
+ if memory_size < required_size:
136
+ raise ValueError(f"Injection size > allocated size: {required_size} > {memory_size}")
137
+
138
+ near = injection_point if pm.is_64_bit and not self.is_long_x64_jump_needed else None
139
+ new_memory_address = allocate(memory_size, near=near)
140
+ try:
141
+ jump_to_new_mem = compile_asm(
142
+ get_long_x64_jump_code(new_memory_address)
143
+ if self.is_long_x64_jump_needed
144
+ else get_simple_jump_code(new_memory_address, injection_point),
134
145
  )
135
-
136
- if jumper_length < self.original_code_length:
137
146
  jump_to_new_mem += b"\x90" * (self.original_code_length - len(jump_to_new_mem))
138
- elif jumper_length > self.original_code_length:
139
- raise ValueError(f"Jumper length > original code length: {jumper_length} > {self.original_code_length}")
140
-
141
- code_to_inject = code_to_inject + jump_back
142
-
143
- logger.debug(f"Injecting {len(code_to_inject)} byte(s) (INJECTION) at 0x{self.new_memory_address:X}")
144
- pm.write_bytes(self.new_memory_address, code_to_inject, len(code_to_inject))
147
+ code_to_inject += compile_asm(
148
+ get_long_x64_jump_code(jump_back_address)
149
+ if pm.is_64_bit
150
+ else get_simple_jump_code(jump_back_address, new_memory_address + len(code_to_inject)),
151
+ )
145
152
 
146
- logger.debug(f"Injecting {len(jump_to_new_mem)} byte(s) (JUMPER) at 0x{self.injection_point:X}")
147
- pm.write_bytes(self.injection_point, jump_to_new_mem, len(jump_to_new_mem))
153
+ logger.debug("Injecting %d byte(s) (INJECTION) at 0x%X", len(code_to_inject), new_memory_address)
154
+ pm.write_bytes(new_memory_address, code_to_inject, len(code_to_inject))
155
+ except Exception:
156
+ pm.free(new_memory_address)
157
+ raise
158
+
159
+ self.original_code = original_code
160
+ self.injection_point = injection_point
161
+ self.new_memory_address = new_memory_address
162
+ logger.debug("Injecting %d byte(s) (JUMPER) at 0x%X", len(jump_to_new_mem), injection_point)
163
+ try:
164
+ pm.write_bytes(injection_point, jump_to_new_mem, len(jump_to_new_mem))
165
+ except Exception:
166
+ self.eject()
167
+ raise
148
168
 
149
169
  @override
150
170
  def eject(self):
151
171
  if self.new_memory_address and self.injection_point:
152
- logger.debug(f"Ejecting {self.original_code_length} byte(s) (JUMPER) from 0x{self.injection_point:X}")
172
+ logger.debug("Ejecting %d byte(s) (JUMPER) from 0x%X", self.original_code_length, self.injection_point)
153
173
  pm.write_bytes(self.injection_point, self.original_code, self.original_code_length)
154
174
 
155
- logger.debug(f"Freeing memory at 0x{self.new_memory_address:X}")
175
+ logger.debug("Freeing memory at 0x%X", self.new_memory_address)
156
176
  pm.free(self.new_memory_address)
157
177
  self.new_memory_address = 0
158
178
  self.injection_point = 0
@@ -166,12 +186,22 @@ class MultipleCodeInjection(AbstractCodeInjection):
166
186
 
167
187
  @override
168
188
  def inject(self):
169
- for injection in self._injections:
170
- injection.inject()
189
+ injected = []
190
+ try:
191
+ for injection in self._injections:
192
+ injection.inject()
193
+ injected.append(injection)
194
+ except Exception:
195
+ for injection in reversed(injected):
196
+ try:
197
+ injection.eject()
198
+ except Exception:
199
+ logger.exception("Failed to roll back injection %r", injection)
200
+ raise
171
201
 
172
202
  @override
173
203
  def eject(self):
174
- for injection in self._injections:
204
+ for injection in reversed(self._injections):
175
205
  injection.eject()
176
206
 
177
207
 
@@ -200,7 +230,7 @@ def preprocess_asm(code: str, *, original_code: bytes | None, base_address: int
200
230
  @lru_cache
201
231
  def compile_asm(code: Code) -> bytes:
202
232
  if isinstance(code, str):
203
- logger.debug(f"Assembling for x{ARCH}:\n{code}")
233
+ logger.debug("Assembling for x%d:\n%s", ARCH, code)
204
234
 
205
235
  fasm_mode = f"use{ARCH}"
206
236
  with TemporaryDirectory() as tmp_dir:
@@ -208,7 +238,7 @@ def compile_asm(code: Code) -> bytes:
208
238
 
209
239
  asm_file.write_text(f"{fasm_mode}\n{code}", encoding="utf-8")
210
240
 
211
- run_executable([FASM_EXECUTABLE_PATH, asm_file], stdout=DEVNULL, check=True)
241
+ run_executable([FASM_EXECUTABLE_PATH, asm_file], stdout=DEVNULL, check=True) # noqa: S603
212
242
  code = asm_file.with_suffix(".bin").read_bytes()
213
243
 
214
244
  if not isinstance(code, bytes):
@@ -218,26 +248,18 @@ def compile_asm(code: Code) -> bytes:
218
248
 
219
249
 
220
250
  def get_simple_jump_code(address: int, ip: int) -> str:
251
+ if pm.is_64_bit and not REL32_MIN_DISPLACEMENT <= address - (ip + REL32_JUMP_SIZE) <= REL32_MAX_DISPLACEMENT:
252
+ raise ValueError(f"Jump from 0x{ip:X} to 0x{address:X} is out of rel32 range")
221
253
  return f"""
222
- jmp {address - ip}
223
- """
224
-
225
-
226
- def get_long_x64_jump_to_new_mem_code(address: int) -> str:
227
- return f"""
228
- push rax
229
- mov rax, {address}
230
- push rax
231
- ret
232
- pop rax
254
+ jmp near {address - ip}
233
255
  """
234
256
 
235
257
 
236
- def get_long_x64_jump_back_code(address: int) -> str:
258
+ def get_long_x64_jump_code(address: int) -> str:
237
259
  return f"""
238
- mov rax, {address}
239
- push rax
240
- ret
260
+ jmp qword [destination]
261
+ destination:
262
+ dq {address}
241
263
  """
242
264
 
243
265
 
@@ -19,11 +19,11 @@ class ASMArrayManager(Sequence):
19
19
  self,
20
20
  array_length: int,
21
21
  element_size: int,
22
- offsets: None | list[int] = None,
22
+ offsets: list[int] | None = None,
23
23
  add: int = 0,
24
24
  ) -> None:
25
- if offsets:
26
- assert element_size == POINTER_SIZE, "If `offsets` is not None, `element_size` must be `POINTER_SIZE`"
25
+ if offsets and element_size != POINTER_SIZE:
26
+ raise ValueError("If `offsets` is not None, `element_size` must be `POINTER_SIZE`")
27
27
 
28
28
  self.array_length = array_length
29
29
  self.element_size = element_size
@@ -46,13 +46,17 @@ class ASMArrayManager(Sequence):
46
46
  if isinstance(index, int):
47
47
  return self.get_address_of_element(index)
48
48
 
49
- return [self.get_address_of_element(i) for i in range(index.start, index.stop, index.step)]
49
+ return [self.get_address_of_element(i) for i in range(*index.indices(len(self)))]
50
50
 
51
51
  def __iter__(self) -> Iterator[Address]:
52
52
  for index in range(len(self)):
53
53
  yield self[index]
54
54
 
55
55
  def get_pointer_to_element(self, index: int) -> int:
56
+ if index < 0:
57
+ index += len(self)
58
+ if not 0 <= index < len(self):
59
+ raise IndexError("Array index out of range")
56
60
  return self.array_address + self.element_size * index
57
61
 
58
62
  def get_address_of_element(self, index: int) -> Address:
@@ -72,7 +76,8 @@ class ASMArrayManager(Sequence):
72
76
  if from_register == array_index_register:
73
77
  array_index_register = reserve_register
74
78
 
75
- assert len({from_register, array_address_register, array_index_register}) == 3, "Registers must be unique"
79
+ if len({from_register, array_address_register, array_index_register}) != 3:
80
+ raise RuntimeError("Registers must be unique")
76
81
 
77
82
  label_continue = f"__label_continue_{uuid4().hex}"
78
83
 
@@ -122,8 +127,10 @@ def regenerate(
122
127
  percent: Number,
123
128
  min_value: Number = 1,
124
129
  ):
125
- if current_value.value < max_value.value:
126
- current_value.value += max(round(max_value.value * percent / 100), min_value)
130
+ current = current_value.value
131
+ maximum = max_value.value
132
+ if current < maximum:
133
+ current_value.value = min(current + max(round(maximum * percent / 100), min_value), maximum)
127
134
 
128
135
 
129
136
  def suppress_memory_exceptions[**P, T](callback: Callable[P, T]) -> Callable[P, T | None]:
@@ -61,8 +61,7 @@ class GradientUpdater(AbstractBaseScript):
61
61
  colors = []
62
62
 
63
63
  for _ in range(length // 2):
64
- for _ in range(self.color_length):
65
- colors.append((red, green, blue))
64
+ colors.extend((red, green, blue) for _ in range(self.color_length))
66
65
 
67
66
  red, delta_red = self.next_color_channel_value(red, delta_red)
68
67
  green, delta_green = self.next_color_channel_value(green, delta_green)
@@ -9,7 +9,7 @@ CONFIG_FILE: Final[Path] = Path("./trainerbase.toml")
9
9
 
10
10
  DEFAULT_VENDOR_FOLDER_PATHS: Final[list[Path]] = [
11
11
  Path(__file__).resolve().parent / "vendor",
12
- Path().resolve() / "trainerbase_vendor",
12
+ Path.cwd() / "trainerbase_vendor",
13
13
  ]
14
14
 
15
15
 
@@ -39,7 +39,7 @@ class GameObject[PymemType, TrainerBaseType](AbstractReadableObject[TrainerBaseT
39
39
  *,
40
40
  frozen: TrainerBaseType | None = None,
41
41
  is_tracked: bool = True,
42
- value_range: None | tuple[TrainerBaseType, TrainerBaseType] = None,
42
+ value_range: tuple[TrainerBaseType, TrainerBaseType] | None = None,
43
43
  ):
44
44
  if is_tracked:
45
45
  GameObject.tracked_objects.append(self)
@@ -186,11 +186,11 @@ class GameBool(GameObject[bool, bool], Switchable):
186
186
 
187
187
  @override
188
188
  def enable(self):
189
- self.value = True # pylint: disable=attribute-defined-outside-init
189
+ self.value = True
190
190
 
191
191
  @override
192
192
  def disable(self):
193
- self.value = False # pylint: disable=attribute-defined-outside-init
193
+ self.value = False
194
194
 
195
195
 
196
196
  class ReadonlyGameObjectSumGetter(AbstractReadableObject[int | float]):
@@ -40,7 +40,7 @@ def simple_trainerbase_menu(
40
40
  )
41
41
  dpg.setup_dearpygui()
42
42
 
43
- with dpg.window(
43
+ with dpg.window( # pyright: ignore[reportGeneralTypeIssues]
44
44
  label=window_title,
45
45
  tag="menu",
46
46
  min_size=[width, height],
@@ -89,7 +89,7 @@ class HorizontalUI(AbstractUIComponent):
89
89
 
90
90
  @override
91
91
  def add_to_ui(self) -> None:
92
- with dpg.group(horizontal=True):
92
+ with dpg.group(horizontal=True): # pyright: ignore[reportGeneralTypeIssues]
93
93
  add_components(*self.components)
94
94
 
95
95
  return super().add_to_ui()
@@ -73,7 +73,7 @@ class GameObjectUI(AbstractUIComponent):
73
73
  if self.set_hotkey is not None:
74
74
  add_hotkey(self.set_hotkey, self.on_value_set)
75
75
 
76
- with dpg.group(horizontal=True):
76
+ with dpg.group(horizontal=True): # pyright: ignore[reportGeneralTypeIssues]
77
77
  if self.gameobject.is_tracked:
78
78
  dpg.add_checkbox(tag=self.dpg_tag_frozen, callback=self.on_frozen_state_change)
79
79
 
@@ -123,15 +123,17 @@ class GameObjectUI(AbstractUIComponent):
123
123
  | GameUnsignedLongLong()
124
124
  ):
125
125
  # There is no input for integers that are not simple `signed long int`.
126
- # TODO: Use better input component if it's already added to dpg. Remove this crutch. # noqa: FIX002
126
+ # TODO: Use better input component if it's already added to dpg. Remove this crutch.
127
+
128
+ if GameInt.value_range is None:
129
+ raise RuntimeError("GameInt must have value_range")
127
130
 
128
- assert GameInt.value_range is not None, "GameInt must have value_range"
129
131
  min_value = default_kwargs.pop("min_value", GameInt.value_range[0])
130
132
  max_value = default_kwargs.pop("max_value", GameInt.value_range[1])
131
133
 
132
134
  if self.gameobject.value_range is not None:
133
- min_value = max(min_value, GameInt.value_range[0]) # type: ignore
134
- max_value = min(max_value, GameInt.value_range[1]) # type: ignore
135
+ min_value = max(min_value, GameInt.value_range[0])
136
+ max_value = min(max_value, GameInt.value_range[1])
135
137
 
136
138
  dpg.add_input_int(min_value=min_value, max_value=max_value, **default_kwargs)
137
139
  case GameBool():
@@ -55,7 +55,7 @@ class SpeedHackUI(AbstractUIComponent):
55
55
  add_components(HotkeyHandlerUI(self.handler, "SpeedHack"))
56
56
 
57
57
  if self.next_preset_hotkey is not None and self.previous_preset_hotkey is not None:
58
- with dpg.group(horizontal=True):
58
+ with dpg.group(horizontal=True): # pyright: ignore[reportGeneralTypeIssues]
59
59
  add_components(
60
60
  HotkeyHandlerUI(
61
61
  SimpleHotkeyHandler(partial(self.change_preset, -1), self.previous_preset_hotkey),
@@ -89,7 +89,7 @@ class TapperUI(AbstractUIComponent):
89
89
  min_value=0,
90
90
  max_value=len(self.DURATION_PRESETS) - 1,
91
91
  clamped=True,
92
- default_value=self.get_closest_preset_index(self.DURATION_PRESETS, self.default_delay),
92
+ default_value=self.get_closest_preset_index(self.DURATION_PRESETS, self.default_duration),
93
93
  callback=self.on_duration_preset_change,
94
94
  )
95
95
 
@@ -111,7 +111,7 @@ class TapperUI(AbstractUIComponent):
111
111
 
112
112
  def on_duration_change(self):
113
113
  new_duration = dpg.get_value(self.dpg_tag_tapper_duration_input)
114
- closest_preset_index = self.get_closest_preset_index(self.DELAY_PRESETS, new_duration)
114
+ closest_preset_index = self.get_closest_preset_index(self.DURATION_PRESETS, new_duration)
115
115
  self.tapper.duration = new_duration
116
116
  dpg.set_value(self.dpg_tag_tapper_duration_preset_input, closest_preset_index)
117
117
 
@@ -127,7 +127,7 @@ class TeleportUI(AbstractUIComponent):
127
127
  if self.hotkey_dash is None:
128
128
  return
129
129
 
130
- with dpg.group(horizontal=True):
130
+ with dpg.group(horizontal=True): # pyright: ignore[reportGeneralTypeIssues]
131
131
  add_hotkey(self.hotkey_dash, self.on_hotkey_dash_press)
132
132
  dpg.add_text(f"[{self.hotkey_dash}] Dash")
133
133
 
@@ -147,7 +147,7 @@ class TeleportUI(AbstractUIComponent):
147
147
  callback=self._on_apply_filter,
148
148
  )
149
149
 
150
- with dpg.group(horizontal=True):
150
+ with dpg.group(horizontal=True): # pyright: ignore[reportGeneralTypeIssues]
151
151
  dpg.add_button(label="Go To", callback=self.on_goto_label)
152
152
  dpg.add_combo(label="Labels", tag=self.DPG_TAG_TELEPORT_LABELS)
153
153
 
@@ -38,7 +38,7 @@ getLogger("comtypes").setLevel(ERROR)
38
38
 
39
39
  logger = getLogger("TrainerBase")
40
40
  if using_default_config:
41
- logger.debug(f"No logging config in {CONFIG_FILE}! Using default one.")
41
+ logger.debug("No logging config in %s! Using default one.", CONFIG_FILE)
42
42
 
43
43
 
44
44
  def _exception_hook(*args):
@@ -22,7 +22,7 @@ from trainerbase.tts import tts_manager
22
22
  type Callback = Callable[[], None]
23
23
 
24
24
 
25
- def run( # pylint: disable=too-complex
25
+ def run(
26
26
  run_gui: Callable[[Callback, Callback], None],
27
27
  on_gui_initialized_hook: Callback | None = None,
28
28
  on_shutdown_hook: Callback | None = None,
@@ -80,6 +80,5 @@ def run( # pylint: disable=too-complex
80
80
  print_exc()
81
81
  except KeyboardInterrupt:
82
82
  logger.info("KeyboardInterrupt (Ctrl+C)")
83
- pass
84
83
 
85
84
  on_shutdown()
@@ -1,5 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from collections.abc import Callable
3
+ from ctypes import Structure, WinDLL, addressof, byref, c_size_t, c_uint16, c_uint32, c_uint64, c_void_p, get_last_error
3
4
  from functools import partial
4
5
  from itertools import count
5
6
  from time import sleep
@@ -8,6 +9,7 @@ from typing import Final, Self, assert_never, cast, override
8
9
 
9
10
  from pymem.pattern import pattern_scan_all, pattern_scan_module
10
11
  from pymem.process import module_from_name
12
+ from pymem.ressources.structure import MEMORY_PROTECTION, MEMORY_STATE
11
13
 
12
14
  from trainerbase.logger import logger
13
15
  from trainerbase.process import pm
@@ -15,7 +17,12 @@ from trainerbase.process import pm
15
17
 
16
18
  ARCH: Final[int] = 64 if pm.is_64_bit else 32
17
19
  POINTER_SIZE: Final[int] = 8 if pm.is_64_bit else 4
18
- DEFAULT_HARDCODED_ADDRESS_OFFSET_READER: Callable[[int], int] = cast(Callable[[int], int], pm.read_uint)
20
+ DEFAULT_HARDCODED_ADDRESS_OFFSET_READER: Final[Callable[[int], int]] = cast(Callable[[int], int], pm.read_uint)
21
+ ANY_BYTE_MARKER_HEX: Final[str] = "2E" # 2E is hex(46), 46 is ord(".")
22
+ REL32_JUMP_SIZE: Final[int] = 5
23
+ REL32_MIN_DISPLACEMENT: Final[int] = -(2**31)
24
+ REL32_MAX_DISPLACEMENT: Final[int] = 2**31 - 1
25
+ MEM_EXTENDED_PARAMETER_ADDRESS_REQUIREMENTS: Final[int] = 1
19
26
 
20
27
 
21
28
  type ConvertibleToAddress = AbstractAddress | int | bytes | str
@@ -24,7 +31,7 @@ type ConvertibleToAddress = AbstractAddress | int | bytes | str
24
31
  read_pointer = cast(Callable[[int], int], pm.read_ulonglong if pm.is_64_bit else pm.read_uint)
25
32
 
26
33
 
27
- logger.debug(f"Process arch is {ARCH}")
34
+ logger.debug("Process arch is %d", ARCH)
28
35
 
29
36
 
30
37
  class AbstractAddress(ABC):
@@ -51,9 +58,10 @@ class AOBScan(AbstractAddress):
51
58
  ):
52
59
  self._should_cache = should_cache
53
60
  self._saved_scan_result: int | None = None
61
+ self._any_byte_marker = any_byte_marker
54
62
 
55
63
  if isinstance(aob, str):
56
- aob = bytes.fromhex(aob.replace(any_byte_marker, "2E")) # 2E is hex(46), 46 is ord(".")
64
+ aob = bytes.fromhex(aob.replace(any_byte_marker, ANY_BYTE_MARKER_HEX)) # 2E is hex(46), 46 is ord(".")
57
65
 
58
66
  if should_escape:
59
67
  aob = self._escape_aob(aob)
@@ -88,7 +96,8 @@ class AOBScan(AbstractAddress):
88
96
  scan_result: list[int] | int | None = find_aob(self.aob, check_memory_protection=self.check_memory_protection)
89
97
 
90
98
  if not scan_result:
91
- raise ValueError(f"AOB not found: {self.aob}")
99
+ aob_repr = self.aob.hex(" ").upper().replace(ANY_BYTE_MARKER_HEX, self._any_byte_marker)
100
+ raise ValueError(f"AOB not found: {aob_repr}")
92
101
 
93
102
  if isinstance(scan_result, list):
94
103
  result_address = scan_result[cast(int, self.multiple_result_index)]
@@ -174,14 +183,79 @@ def ensure_address(obj: ConvertibleToAddress) -> AbstractAddress:
174
183
  case bytes() | str():
175
184
  return AOBScan(obj)
176
185
  case _:
177
- raise TypeError(f"Cannot create AbstractAddress from {type(obj)}")
186
+ assert_never(obj)
178
187
 
179
188
 
180
- def allocate(size: int = POINTER_SIZE, count: int = 1) -> int:
181
- total = size * count
182
- logger.debug(f"Allocating {total} bytes.")
189
+ class _SystemInfo(Structure):
190
+ _fields_ = [
191
+ ("architecture", c_uint16),
192
+ ("reserved", c_uint16),
193
+ ("page_size", c_uint32),
194
+ ("minimum_address", c_void_p),
195
+ ("maximum_address", c_void_p),
196
+ ("processor_mask", c_size_t),
197
+ ("processor_count", c_uint32),
198
+ ("processor_type", c_uint32),
199
+ ("allocation_granularity", c_uint32),
200
+ ("processor_level", c_uint16),
201
+ ("processor_revision", c_uint16),
202
+ ]
203
+
204
+
205
+ class _AddressRequirements(Structure):
206
+ _fields_ = [("lowest", c_void_p), ("highest", c_void_p), ("alignment", c_size_t)]
207
+
208
+
209
+ class _ExtendedParameter(Structure):
210
+ _fields_ = [("type", c_uint64), ("pointer", c_uint64)]
183
211
 
184
- return pm.allocate(total)
212
+
213
+ _kernelbase = WinDLL("kernelbase", use_last_error=True)
214
+ _get_system_info = _kernelbase.GetSystemInfo
215
+ _get_system_info.argtypes = [c_void_p]
216
+ _get_system_info.restype = None
217
+ _virtual_alloc2 = _kernelbase.VirtualAlloc2
218
+ _virtual_alloc2.argtypes = [c_void_p, c_void_p, c_size_t, c_uint32, c_uint32, c_void_p, c_uint32]
219
+ _virtual_alloc2.restype = c_void_p
220
+ _system_info = _SystemInfo()
221
+ _get_system_info(byref(_system_info))
222
+
223
+
224
+ def allocate(size: int = POINTER_SIZE, count: int = 1, *, near: ConvertibleToAddress | None = None) -> int:
225
+ total = size * count
226
+ logger.debug("Allocating %d bytes.", total)
227
+
228
+ if near is None:
229
+ return pm.allocate(total)
230
+
231
+ if total <= 0:
232
+ raise ValueError("Allocation size must be positive")
233
+
234
+ address = ensure_address(near).resolve()
235
+ granularity = _system_info.allocation_granularity
236
+ page_size = _system_info.page_size
237
+ lowest = max(_system_info.minimum_address, address + REL32_JUMP_SIZE + REL32_MIN_DISPLACEMENT)
238
+ highest = min(_system_info.maximum_address, address + REL32_JUMP_SIZE + REL32_MAX_DISPLACEMENT)
239
+ lowest = (lowest + granularity - 1) // granularity * granularity
240
+ highest = (highest + 1) // granularity * granularity - 1
241
+ total = (total + page_size - 1) // page_size * page_size
242
+ if highest - lowest + 1 < total:
243
+ raise ValueError(f"No allocation range near 0x{address:X}")
244
+
245
+ requirements = _AddressRequirements(lowest=lowest, highest=highest, alignment=granularity)
246
+ parameter = _ExtendedParameter(MEM_EXTENDED_PARAMETER_ADDRESS_REQUIREMENTS, addressof(requirements))
247
+ allocated = _virtual_alloc2(
248
+ pm.process_handle,
249
+ None, # BaseAddress: let Windows choose an address within requirements.
250
+ total,
251
+ MEMORY_STATE.MEM_RESERVE | MEMORY_STATE.MEM_COMMIT,
252
+ MEMORY_PROTECTION.PAGE_EXECUTE_READWRITE,
253
+ byref(parameter),
254
+ 1, # ParameterCount: one address-requirements parameter.
255
+ )
256
+ if not allocated:
257
+ raise ValueError(f"Cannot allocate {total} bytes near 0x{address:X}: WinError {get_last_error()}")
258
+ return allocated
185
259
 
186
260
 
187
261
  def read_hardcoded_static_address(
@@ -239,19 +313,19 @@ def get_module_address(module_name: str, *, delay: float = 2, retry_limit: int |
239
313
  Set retry_limit to 0 if you want to disable retries.
240
314
  """
241
315
 
242
- logger.info(f"Looking for {module_name}...")
316
+ logger.info("Looking for %s...", module_name)
243
317
 
244
318
  for try_counter in count():
245
319
  module_info = module_from_name(pm.process_handle, module_name)
246
320
 
247
321
  if module_info is not None:
248
- logger.info(f"Successfully found {module_name} at 0x{module_info.lpBaseOfDll:X}")
322
+ logger.info("Successfully found %s at 0x%X", module_name, module_info.lpBaseOfDll)
249
323
  return module_info.lpBaseOfDll
250
324
 
251
325
  if try_counter == retry_limit:
252
326
  raise RuntimeError(f"Failed to find module: {module_name}, tried {try_counter + 1} time(s).")
253
327
 
254
- logger.info(f"Failed to find {module_name}. I will retry in {delay} s. Retry limit is {retry_limit}")
328
+ logger.info("Failed to find %s. I will retry in %f s. Retry limit is %s", module_name, delay, retry_limit)
255
329
 
256
330
  sleep(delay)
257
331
 
@@ -273,7 +347,7 @@ def ensure_resolved(
273
347
  Set retry_limit to 0 if you want to disable retries.
274
348
  """
275
349
 
276
- logger.info(f"Looking for {verbose_name}...")
350
+ logger.info("Looking for %s...", verbose_name)
277
351
 
278
352
  for try_counter in count():
279
353
  try:
@@ -282,12 +356,14 @@ def ensure_resolved(
282
356
  if try_counter == retry_limit:
283
357
  raise RuntimeError(f"Failed to resolve {verbose_name}, tried {try_counter + 1} time(s).") from e
284
358
 
285
- logger.info(f"Failed to resolve {verbose_name}. I will retry in {delay} s. Retry limit is {retry_limit}")
359
+ logger.info(
360
+ "Failed to resolve %s. I will retry in %f s. Retry limit is %s", verbose_name, delay, retry_limit
361
+ )
286
362
 
287
363
  sleep(delay)
288
364
  continue
289
365
 
290
- logger.info(f"Successfully found {verbose_name} at 0x{resolved:X}")
366
+ logger.info("Successfully found %s at 0x%X", verbose_name, resolved)
291
367
  return resolved
292
368
 
293
369
  raise RuntimeError("This code must be unreachable!")
@@ -46,17 +46,19 @@ def select_one_process(processes: Iterable[Process]) -> Process:
46
46
 
47
47
 
48
48
  def get_target_pid(process_names: Iterable[str], *, exact_match: bool, ignore_case: bool) -> int:
49
- logger.info(f"Waiting for process in: {process_names}")
49
+ logger.info("Waiting for process in: %s", process_names)
50
50
 
51
51
  for process_name in cycle(process_names):
52
52
  found_processes = find_processes(process_name, exact_match=exact_match, ignore_case=ignore_case)
53
53
 
54
54
  if found_processes:
55
55
  pid = select_one_process(found_processes).pid
56
- logger.info(f"Found {process_name}, pid: {pid}")
56
+ logger.info("Found %s, pid: %d", process_name, pid)
57
57
 
58
58
  return pid
59
59
 
60
+ sleep(0.1)
61
+
60
62
  raise RuntimeError("This place must be unreachable! `process_names` is probably empty.")
61
63
 
62
64
 
@@ -1,14 +1,13 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from collections.abc import Callable
3
- from contextlib import suppress
4
- from sys import stderr
5
- from threading import Thread
3
+ from threading import Thread, current_thread
6
4
  from time import sleep
7
5
  from typing import Any, override
8
6
 
9
7
  from pymem.exception import MemoryReadError, MemoryWriteError
10
8
 
11
9
  from trainerbase.abc import Switchable
10
+ from trainerbase.logger import logger
12
11
 
13
12
 
14
13
  class AbstractBaseScript(Switchable, ABC):
@@ -54,18 +53,23 @@ class ScriptEngine:
54
53
  return f"<ScriptEngine delay={self.delay} should_run={self.should_run} scripts={len(self.scripts)}>"
55
54
 
56
55
  def start(self):
56
+ if self.thread.is_alive():
57
+ return
58
+ self.thread = Thread(target=self.script_loop)
57
59
  self.should_run = True
58
60
  self.thread.start()
59
61
 
60
62
  def stop(self):
61
63
  self.should_run = False
62
64
 
63
- with suppress(RuntimeError):
65
+ if self.thread.ident is not None and self.thread is not current_thread():
64
66
  self.thread.join()
65
67
 
66
68
  def script_loop(self):
67
69
  while self.should_run:
68
70
  for script in self.scripts:
71
+ if not self.should_run:
72
+ return
69
73
  if not script.enabled:
70
74
  continue
71
75
 
@@ -73,9 +77,10 @@ class ScriptEngine:
73
77
  script()
74
78
  except MemoryReadError, MemoryWriteError:
75
79
  continue
76
- except Exception as e: # pylint: disable=broad-exception-caught
77
- print(e, file=stderr)
80
+ except Exception:
81
+ logger.exception("Error in script %r. Stopping Script Engine.", script)
78
82
  self.stop()
83
+ return
79
84
 
80
85
  sleep(self.delay)
81
86
 
File without changes