trainerbase 4.8.2__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.2 → trainerbase-4.9.0}/PKG-INFO +3 -3
  2. trainerbase-4.9.0/pyproject.toml +92 -0
  3. trainerbase-4.8.2/pyproject.toml → trainerbase-4.9.0/pyproject.toml.orig +8 -6
  4. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/codeinjection.py +76 -54
  5. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/common/helpers.py +10 -4
  6. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gameobject.py +1 -1
  7. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/helpers.py +1 -1
  8. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/misc.py +1 -1
  9. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/objects.py +1 -1
  10. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/speedhack.py +1 -1
  11. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/tapper.py +2 -2
  12. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/teleport.py +2 -2
  13. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/memory.py +73 -2
  14. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/process.py +2 -0
  15. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/scriptengine.py +8 -3
  16. {trainerbase-4.8.2 → trainerbase-4.9.0}/README.md +0 -0
  17. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/__init__.py +0 -0
  18. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/abc.py +0 -0
  19. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/common/__init__.py +0 -0
  20. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/common/abc.py +0 -0
  21. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/common/keyboard.py +0 -0
  22. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/common/rainbow.py +0 -0
  23. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/common/tapper.py +0 -0
  24. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/common/teleport.py +0 -0
  25. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/config.py +0 -0
  26. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/__init__.py +0 -0
  27. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/injections.py +0 -0
  28. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/rainbow.py +0 -0
  29. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/scripts.py +0 -0
  30. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/types.py +0 -0
  31. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/logger.py +0 -0
  32. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/main.py +0 -0
  33. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/py.typed +0 -0
  34. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/speedhack.py +0 -0
  35. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/tts.py +0 -0
  36. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/vendor/FASM.EXE +0 -0
  37. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/vendor/big_logo.png +0 -0
  38. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/vendor/speedhack32.dll +0 -0
  39. {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/vendor/speedhack64.dll +0 -0
  40. {trainerbase-4.8.2 → 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.2
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.2"
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",
@@ -53,6 +53,8 @@ ignore = [
53
53
  "PLW2901",
54
54
  "TD002",
55
55
  "TD003",
56
+ "CPY001",
57
+ "PLR0917",
56
58
  ]
57
59
 
58
60
  [tool.ruff.lint.isort]
@@ -80,7 +82,7 @@ build-backend = "uv_build"
80
82
 
81
83
  [dependency-groups]
82
84
  dev = [
83
- "invoke>=2.2.1",
84
- "pyright>=1.1.407",
85
- "ruff>=0.15.9",
85
+ "invoke>=3.0.3",
86
+ "pyright>=1.1.414",
87
+ "ruff>=0.17.0",
86
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,6 +65,9 @@ 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
 
@@ -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,50 +110,61 @@ 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("Injecting %d byte(s) (INJECTION) at 0x%X", len(code_to_inject), self.new_memory_address)
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("Injecting %d byte(s) (JUMPER) at 0x%X", len(jump_to_new_mem), self.injection_point)
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):
@@ -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
 
@@ -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,7 +19,7 @@ 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
25
  if offsets and element_size != POINTER_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:
@@ -123,8 +127,10 @@ def regenerate(
123
127
  percent: Number,
124
128
  min_value: Number = 1,
125
129
  ):
126
- if current_value.value < max_value.value:
127
- 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)
128
134
 
129
135
 
130
136
  def suppress_memory_exceptions[**P, T](callback: Callable[P, T]) -> Callable[P, T | None]:
@@ -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)
@@ -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
 
@@ -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
 
@@ -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
@@ -17,6 +19,10 @@ 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
20
  DEFAULT_HARDCODED_ADDRESS_OFFSET_READER: Final[Callable[[int], int]] = cast(Callable[[int], int], pm.read_uint)
19
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
20
26
 
21
27
 
22
28
  type ConvertibleToAddress = AbstractAddress | int | bytes | str
@@ -180,11 +186,76 @@ def ensure_address(obj: ConvertibleToAddress) -> AbstractAddress:
180
186
  assert_never(obj)
181
187
 
182
188
 
183
- def allocate(size: int = POINTER_SIZE, count: int = 1) -> int:
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)]
211
+
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:
184
225
  total = size * count
185
226
  logger.debug("Allocating %d bytes.", total)
186
227
 
187
- return pm.allocate(total)
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
188
259
 
189
260
 
190
261
  def read_hardcoded_static_address(
@@ -57,6 +57,8 @@ def get_target_pid(process_names: Iterable[str], *, exact_match: bool, ignore_ca
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,7 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from collections.abc import Callable
3
- from contextlib import suppress
4
- from threading import Thread
3
+ from threading import Thread, current_thread
5
4
  from time import sleep
6
5
  from typing import Any, override
7
6
 
@@ -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
 
@@ -76,6 +80,7 @@ class ScriptEngine:
76
80
  except Exception:
77
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