trainerbase 4.5.0__tar.gz → 4.6.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.5.0 → trainerbase-4.6.0}/PKG-INFO +3 -2
  2. {trainerbase-4.5.0 → trainerbase-4.6.0}/pyproject.toml +4 -3
  3. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/codeinjection.py +61 -11
  4. trainerbase-4.6.0/trainerbase/config.py +56 -0
  5. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/logger.py +3 -6
  6. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/memory.py +38 -0
  7. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/process.py +7 -7
  8. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/speedhack.py +4 -3
  9. trainerbase-4.5.0/trainerbase/config.py +0 -29
  10. {trainerbase-4.5.0 → trainerbase-4.6.0}/.gitignore +0 -0
  11. {trainerbase-4.5.0 → trainerbase-4.6.0}/LICENSE +0 -0
  12. {trainerbase-4.5.0 → trainerbase-4.6.0}/README.md +0 -0
  13. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/__init__.py +0 -0
  14. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/abc.py +0 -0
  15. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/common/__init__.py +0 -0
  16. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/common/abc.py +0 -0
  17. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/common/helpers.py +0 -0
  18. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/common/keyboard.py +0 -0
  19. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/common/rainbow.py +0 -0
  20. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/common/tapper.py +0 -0
  21. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/common/teleport.py +0 -0
  22. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/gameobject.py +0 -0
  23. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/gui/__init__.py +0 -0
  24. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/gui/helpers.py +0 -0
  25. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/gui/injections.py +0 -0
  26. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/gui/misc.py +0 -0
  27. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/gui/objects.py +0 -0
  28. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/gui/rainbow.py +0 -0
  29. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/gui/scripts.py +0 -0
  30. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/gui/speedhack.py +0 -0
  31. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/gui/tapper.py +0 -0
  32. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/gui/teleport.py +0 -0
  33. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/gui/types.py +0 -0
  34. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/main.py +0 -0
  35. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/py.typed +0 -0
  36. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/scriptengine.py +0 -0
  37. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase/tts.py +0 -0
  38. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase_vendor/FASM.EXE +0 -0
  39. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase_vendor/speedhack32.dll +0 -0
  40. {trainerbase-4.5.0 → trainerbase-4.6.0}/trainerbase_vendor/speedhack64.dll +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trainerbase
3
- Version: 4.5.0
3
+ Version: 4.6.0
4
4
  Summary: Framework for creating game trainers/mods
5
5
  Project-URL: Homepage, https://gitlab.com/python-trainers/
6
6
  Project-URL: Repository, https://gitlab.com/python-trainers/trainer-base
@@ -13,7 +13,8 @@ Requires-Python: >=3.13
13
13
  Requires-Dist: dearpygui>=2.0.0
14
14
  Requires-Dist: keyboard>=0.13.5
15
15
  Requires-Dist: mouse>=0.7.1
16
- Requires-Dist: psutil>=6.1.1
16
+ Requires-Dist: psutil>=7.0.0
17
+ Requires-Dist: pydantic>=2.10.6
17
18
  Requires-Dist: pyinjector>=1.3.0
18
19
  Requires-Dist: pymem>=1.14.0
19
20
  Requires-Dist: pyttsx3>=2.98
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "trainerbase"
3
- version = "4.5.0"
3
+ version = "4.6.0"
4
4
  description = "Framework for creating game trainers/mods"
5
5
  license = "MIT"
6
6
  authors = [
@@ -13,7 +13,8 @@ dependencies = [
13
13
  "dearpygui>=2.0.0",
14
14
  "keyboard>=0.13.5",
15
15
  "mouse>=0.7.1",
16
- "psutil>=6.1.1",
16
+ "psutil>=7.0.0",
17
+ "pydantic>=2.10.6",
17
18
  "pyinjector>=1.3.0",
18
19
  "pymem>=1.14.0",
19
20
  "pyttsx3>=2.98",
@@ -73,5 +74,5 @@ build-backend = "hatchling.build"
73
74
 
74
75
  [dependency-groups]
75
76
  dev = [
76
- "ruff>=0.8.6",
77
+ "ruff>=0.9.7",
77
78
  ]
@@ -1,5 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from contextlib import suppress
3
+ from functools import lru_cache
3
4
  from pathlib import Path
4
5
  from subprocess import DEVNULL
5
6
  from subprocess import run as run_executable
@@ -8,12 +9,16 @@ from typing import ClassVar, Final, Self, override
8
9
 
9
10
  from pymem.exception import MemoryWriteError
10
11
 
12
+ from trainerbase.config import config
11
13
  from trainerbase.logger import logger
12
14
  from trainerbase.memory import ARCH, ConvertibleToAddress, allocate, ensure_address
13
15
  from trainerbase.process import pm
14
16
 
15
17
 
16
- FASM_EXECUTABLE_PATH: Final[Path] = Path(__file__).resolve().parent.parent / "trainerbase_vendor" / "FASM.EXE"
18
+ FASM_EXECUTABLE_PATH: Final[Path] = config.vendor.folder / "FASM.EXE"
19
+ ORIGINAL_CODE_PLACEHOLDER: Final[str] = "__original_code__"
20
+ BASE_ADDRESS_PLACEHOLDER: Final[str] = "__base_address__"
21
+ MOVE_X64_MACROS: Final[str] = "__move_x64__"
17
22
 
18
23
 
19
24
  type Code = bytes | str
@@ -70,8 +75,8 @@ class CodeInjection(AbstractCodeInjection):
70
75
  logger.debug("Nothing to eject")
71
76
 
72
77
 
73
- class AllocatingCodeInjection(AbstractCodeInjection): # pylint: disable=too-many-instance-attributes
74
- def __init__( # pylint: disable=too-many-arguments
78
+ class AllocatingCodeInjection(AbstractCodeInjection):
79
+ def __init__(
75
80
  self,
76
81
  address: ConvertibleToAddress,
77
82
  code_to_inject: Code,
@@ -85,10 +90,7 @@ class AllocatingCodeInjection(AbstractCodeInjection): # pylint: disable=too-man
85
90
 
86
91
  self.is_long_x64_jump_needed = is_long_x64_jump_needed
87
92
  self.address = ensure_address(address)
88
- self.code_to_inject = compile_asm(code_to_inject)
89
-
90
- if self.is_long_x64_jump_needed:
91
- self.code_to_inject = compile_asm("pop rax") + self.code_to_inject + compile_asm("push rax")
93
+ self.code_to_inject = code_to_inject
92
94
 
93
95
  self.original_code: bytes = b""
94
96
 
@@ -100,12 +102,25 @@ class AllocatingCodeInjection(AbstractCodeInjection): # pylint: disable=too-man
100
102
  @override
101
103
  def inject(self):
102
104
  self.injection_point = self.address.resolve()
103
-
104
105
  self.new_memory_address: int = allocate(self.new_memory_size)
105
-
106
106
  self.original_code = pm.read_bytes(self.injection_point, self.original_code_length)
107
107
 
108
+ code_to_inject = self.code_to_inject
109
+
110
+ if isinstance(code_to_inject, str):
111
+ code_to_inject = preprocess_asm(
112
+ code_to_inject,
113
+ original_code=self.original_code,
114
+ base_address=pm.base_address,
115
+ )
116
+
117
+ if isinstance(code_to_inject, bytearray):
118
+ raise TypeError("Code to inject must not be bytearray")
119
+
120
+ code_to_inject = compile_asm(code_to_inject)
121
+
108
122
  if self.is_long_x64_jump_needed:
123
+ code_to_inject = compile_asm("pop rax") + code_to_inject + compile_asm("push rax")
109
124
  jump_to_new_mem = compile_asm(get_long_x64_jump_to_new_mem_code(self.new_memory_address))
110
125
  jumper_length = len(jump_to_new_mem)
111
126
  jump_back_address = self.injection_point + jumper_length - 1
@@ -115,7 +130,7 @@ class AllocatingCodeInjection(AbstractCodeInjection): # pylint: disable=too-man
115
130
  jumper_length = len(jump_to_new_mem)
116
131
  jump_back_address = self.injection_point + jumper_length
117
132
  jump_back = compile_asm(
118
- get_simple_jump_code(jump_back_address, self.new_memory_address + len(self.code_to_inject)),
133
+ get_simple_jump_code(jump_back_address, self.new_memory_address + len(code_to_inject)),
119
134
  )
120
135
 
121
136
  if jumper_length < self.original_code_length:
@@ -123,7 +138,7 @@ class AllocatingCodeInjection(AbstractCodeInjection): # pylint: disable=too-man
123
138
  elif jumper_length > self.original_code_length:
124
139
  raise ValueError(f"Jumper length > original code length: {jumper_length} > {self.original_code_length}")
125
140
 
126
- code_to_inject = self.code_to_inject + jump_back
141
+ code_to_inject = code_to_inject + jump_back
127
142
 
128
143
  logger.debug(f"Injecting {len(code_to_inject)} byte(s) (INJECTION) at 0x{self.new_memory_address:X}")
129
144
  pm.write_bytes(self.new_memory_address, code_to_inject, len(code_to_inject))
@@ -160,6 +175,29 @@ class MultipleCodeInjection(AbstractCodeInjection):
160
175
  injection.eject()
161
176
 
162
177
 
178
+ def preprocess_asm(code: str, *, original_code: bytes | None, base_address: int | None) -> str:
179
+ new_lines = []
180
+
181
+ for line in code.splitlines():
182
+ if original_code is not None:
183
+ formatted_bytes = ", ".join(map(hex, original_code))
184
+ formatted_original_code = f"db {formatted_bytes}"
185
+
186
+ line = line.replace(ORIGINAL_CODE_PLACEHOLDER, formatted_original_code)
187
+
188
+ if base_address is not None:
189
+ line = line.replace(BASE_ADDRESS_PLACEHOLDER, hex(base_address))
190
+
191
+ if MOVE_X64_MACROS in line:
192
+ _, address, register = line.strip().split()
193
+ line = get_move_x64_register_to_memory_code(address, register)
194
+
195
+ new_lines.append(line)
196
+
197
+ return "\n".join(new_lines)
198
+
199
+
200
+ @lru_cache
163
201
  def compile_asm(code: Code) -> bytes:
164
202
  if isinstance(code, str):
165
203
  logger.debug(f"Assembling for x{ARCH}:\n{code}")
@@ -203,6 +241,18 @@ def get_long_x64_jump_back_code(address: int) -> str:
203
241
  """
204
242
 
205
243
 
244
+ def get_move_x64_register_to_memory_code(address: int | str, register: str) -> str:
245
+ temporary_register = "rbx" if register == "rax" else "rax"
246
+ code = f"""
247
+ push {temporary_register}
248
+ mov {temporary_register}, {address}
249
+ mov [{temporary_register}], {register}
250
+ pop {temporary_register}
251
+ """
252
+
253
+ return code
254
+
255
+
206
256
  def safely_eject_all_code_injections() -> None:
207
257
  for injection in AbstractCodeInjection.created_code_injections:
208
258
  with suppress(MemoryWriteError):
@@ -0,0 +1,56 @@
1
+ from pathlib import Path
2
+ from tomllib import load as load_toml
3
+ from typing import Any, Final
4
+
5
+ from pydantic import AliasChoices, BaseModel, Field
6
+
7
+
8
+ CONFIG_FILE: Final[Path] = Path("./trainerbase.toml")
9
+
10
+ DEFAULT_VENDOR_FOLDER_PATHS: Final[list[Path]] = [
11
+ Path(__file__).resolve().parent.parent / "trainerbase_vendor",
12
+ Path().resolve() / "trainerbase_vendor",
13
+ ]
14
+
15
+
16
+ class Process(BaseModel):
17
+ names: list[str] = Field(validation_alias=AliasChoices("names", "process_names"))
18
+ exact_match: bool = True
19
+ ignore_case: bool = False
20
+
21
+
22
+ class SpeedhackConfig(BaseModel):
23
+ offset_x64: int = 0x47098
24
+ offset_x32: int = 0x40058
25
+
26
+
27
+ class Vendor(BaseModel):
28
+ extra_search_paths: list[Path] | None = None
29
+ speedhack: SpeedhackConfig = SpeedhackConfig()
30
+
31
+ @property
32
+ def folder(self) -> Path:
33
+ search_paths = DEFAULT_VENDOR_FOLDER_PATHS.copy()
34
+
35
+ if self.extra_search_paths is not None:
36
+ search_paths.extend(self.extra_search_paths)
37
+
38
+ try:
39
+ path = next(filter(Path.is_dir, search_paths))
40
+ except StopIteration as e:
41
+ raise ValueError("Failed to find Trainer Base vendor folder") from e
42
+
43
+ return path.resolve()
44
+
45
+
46
+ class Config(BaseModel):
47
+ process: Process = Field(validation_alias=AliasChoices("process", "pymem"))
48
+ vendor: Vendor = Vendor()
49
+ logging: dict[str, Any] | None = None # Must be managed by logging.dictConfig
50
+
51
+
52
+ with CONFIG_FILE.resolve().open("rb") as trainerbase_toml:
53
+ trainerbase_config = load_toml(trainerbase_toml)
54
+
55
+
56
+ config = Config.model_validate(trainerbase_config)
@@ -1,10 +1,10 @@
1
1
  from logging import ERROR, getLogger
2
2
  from logging.config import dictConfig
3
3
 
4
- from trainerbase.config import CONFIG_FILE, config_warnings, logging_config
4
+ from trainerbase.config import CONFIG_FILE, config
5
5
 
6
6
 
7
- if logging_config is None:
7
+ if config.logging is None:
8
8
  logging_config = {
9
9
  "version": 1,
10
10
  "disable_existing_loggers": True,
@@ -28,9 +28,9 @@ if logging_config is None:
28
28
  }
29
29
  using_default_config = True
30
30
  else:
31
+ logging_config = config.logging
31
32
  using_default_config = False
32
33
 
33
-
34
34
  dictConfig(logging_config)
35
35
  getLogger("comtypes").setLevel(ERROR)
36
36
 
@@ -38,6 +38,3 @@ logger = getLogger("TrainerBase")
38
38
 
39
39
  if using_default_config:
40
40
  logger.debug(f"No logging config in {CONFIG_FILE}! Using default one.")
41
-
42
- for warning_message in config_warnings:
43
- logger.warning(warning_message)
@@ -207,3 +207,41 @@ def get_module_address(module_name: str, *, delay: float = 2, retry_limit: int |
207
207
  sleep(delay)
208
208
 
209
209
  raise RuntimeError("This code must be unreachable!")
210
+
211
+
212
+ def ensure_resolved(
213
+ address: AbstractAddress,
214
+ verbose_name: str,
215
+ *,
216
+ delay: float = 2,
217
+ retry_limit: int | None = 4,
218
+ ) -> int:
219
+ """
220
+ Returns resolved address.
221
+ Raises RuntimeError if the address was not found and retry_limit is reached.
222
+
223
+ Set retry_limit to None or negative value if you need infinite retry cycle.
224
+ Set retry_limit to 0 if you want to disable retries.
225
+ """
226
+
227
+ logger.info(f"Looking for {verbose_name}...")
228
+
229
+ for try_counter in count():
230
+ try:
231
+ resolved = address.resolve()
232
+ except ValueError as e:
233
+ if try_counter == retry_limit:
234
+ raise RuntimeError(f"Failed to resolve {verbose_name}, tried {try_counter + 1} time(s).") from e
235
+
236
+ logger.info(f"Failed to resolve {verbose_name}. I will retry in {delay} s. Retry limit is {retry_limit}")
237
+
238
+ sleep(delay)
239
+ continue
240
+
241
+ logger.info(f"Successfully found {verbose_name} at 0x{resolved:X}")
242
+ return resolved
243
+
244
+ raise RuntimeError("This code must be unreachable!")
245
+
246
+
247
+ mainaob = partial(AOBScan, module_name=pm.process_base.name)
@@ -9,7 +9,7 @@ from psutil import Process, pid_exists, process_iter
9
9
  from pymem import Pymem
10
10
  from pymem.exception import ProcessError
11
11
 
12
- from trainerbase.config import process_config
12
+ from trainerbase.config import config
13
13
  from trainerbase.logger import logger
14
14
 
15
15
 
@@ -70,15 +70,15 @@ def ensure_main_module_is_initialized(pm: Pymem) -> int:
70
70
 
71
71
 
72
72
  def attach_to_process() -> Pymem:
73
- process_names = process_config["names"]
74
- exact_match = process_config["exact_match"]
75
- ignore_case = process_config["ignore_case"]
76
-
77
- if not process_names:
73
+ if not config.process.names:
78
74
  raise ValueError("Empty process name list")
79
75
 
80
76
  with suppress(KeyboardInterrupt):
81
- pid = get_target_pid(process_names, exact_match=exact_match, ignore_case=ignore_case)
77
+ pid = get_target_pid(
78
+ config.process.names,
79
+ exact_match=config.process.exact_match,
80
+ ignore_case=config.process.ignore_case,
81
+ )
82
82
 
83
83
  pm = Pymem(pid)
84
84
  ensure_main_module_is_initialized(pm)
@@ -5,16 +5,17 @@ from typing import Final, Self, cast
5
5
  from pyinjector import inject
6
6
  from pymem.exception import MemoryWriteError
7
7
 
8
+ from trainerbase.config import config
8
9
  from trainerbase.gameobject import GameDouble
9
10
  from trainerbase.memory import ARCH, get_module_address
10
11
  from trainerbase.process import pm
11
12
 
12
13
 
13
14
  SPEEDHACK_DLL_MODULE_NAME: Final[str] = f"speedhack{ARCH}.dll"
14
- SPEEDHACK_DLL_PATH: Final[Path] = (
15
- Path(__file__).resolve().parent.parent / "trainerbase_vendor" / SPEEDHACK_DLL_MODULE_NAME
15
+ SPEEDHACK_DLL_PATH: Final[Path] = config.vendor.folder / SPEEDHACK_DLL_MODULE_NAME
16
+ SPEED_MODIFIER_OFFSET: Final[int] = (
17
+ config.vendor.speedhack.offset_x32 if ARCH == 32 else config.vendor.speedhack.offset_x64
16
18
  )
17
- SPEED_MODIFIER_OFFSET: Final[int] = 0x40058 if ARCH == 32 else 0x47098
18
19
 
19
20
 
20
21
  class SpeedHack:
@@ -1,29 +0,0 @@
1
- from pathlib import Path
2
- from tomllib import load as load_toml
3
- from typing import Final
4
-
5
-
6
- CONFIG_FILE: Final[Path] = Path("./trainerbase.toml")
7
-
8
-
9
- with CONFIG_FILE.resolve().open("rb") as trainerbase_toml:
10
- trainerbase_config = load_toml(trainerbase_toml)
11
-
12
-
13
- config_warnings = []
14
-
15
-
16
- try:
17
- process_config = trainerbase_config["process"]
18
- except KeyError:
19
- # Legacy section support
20
- process_config = trainerbase_config["pymem"]
21
- process_config["names"] = process_config["process_names"]
22
- del trainerbase_config["pymem"]
23
-
24
- config_warnings.append(
25
- "Deprecated config! Use [process] section with field `names` instead of [pymem] with `process_names`"
26
- )
27
-
28
-
29
- logging_config = trainerbase_config.get("logging")
File without changes
File without changes
File without changes