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.
- {trainerbase-4.8.2 → trainerbase-4.9.0}/PKG-INFO +3 -3
- trainerbase-4.9.0/pyproject.toml +92 -0
- trainerbase-4.8.2/pyproject.toml → trainerbase-4.9.0/pyproject.toml.orig +8 -6
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/codeinjection.py +76 -54
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/common/helpers.py +10 -4
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gameobject.py +1 -1
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/helpers.py +1 -1
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/misc.py +1 -1
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/objects.py +1 -1
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/speedhack.py +1 -1
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/tapper.py +2 -2
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/teleport.py +2 -2
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/memory.py +73 -2
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/process.py +2 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/scriptengine.py +8 -3
- {trainerbase-4.8.2 → trainerbase-4.9.0}/README.md +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/__init__.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/abc.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/common/__init__.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/common/abc.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/common/keyboard.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/common/rainbow.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/common/tapper.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/common/teleport.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/config.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/__init__.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/injections.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/rainbow.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/scripts.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/gui/types.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/logger.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/main.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/py.typed +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/speedhack.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/tts.py +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/vendor/FASM.EXE +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/vendor/big_logo.png +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/vendor/speedhack32.dll +0 -0
- {trainerbase-4.8.2 → trainerbase-4.9.0}/trainerbase/vendor/speedhack64.dll +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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>=
|
|
84
|
-
"pyright>=1.1.
|
|
85
|
-
"ruff>=0.
|
|
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
|
|
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 =
|
|
86
|
-
is_long_x64_jump_needed: bool
|
|
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
|
-
|
|
105
|
-
|
|
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=
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
else
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
|
258
|
+
def get_long_x64_jump_code(address: int) -> str:
|
|
237
259
|
return f"""
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
127
|
-
|
|
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:
|
|
42
|
+
value_range: tuple[TrainerBaseType, TrainerBaseType] | None = None,
|
|
43
43
|
):
|
|
44
44
|
if is_tracked:
|
|
45
45
|
GameObject.tracked_objects.append(self)
|
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from collections.abc import Callable
|
|
3
|
-
from
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|