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.
- {trainerbase-4.8.1 → trainerbase-4.9.0}/PKG-INFO +3 -3
- trainerbase-4.9.0/pyproject.toml +92 -0
- trainerbase-4.8.1/pyproject.toml → trainerbase-4.9.0/pyproject.toml.orig +27 -20
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/codeinjection.py +82 -60
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/common/helpers.py +14 -7
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/common/rainbow.py +1 -2
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/config.py +1 -1
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gameobject.py +3 -3
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/helpers.py +1 -1
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/misc.py +1 -1
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/objects.py +7 -5
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/speedhack.py +1 -1
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/tapper.py +2 -2
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/teleport.py +2 -2
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/logger.py +1 -1
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/main.py +1 -2
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/memory.py +91 -15
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/process.py +4 -2
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/scriptengine.py +11 -6
- {trainerbase-4.8.1 → trainerbase-4.9.0}/README.md +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/__init__.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/abc.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/common/__init__.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/common/abc.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/common/keyboard.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/common/tapper.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/common/teleport.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/__init__.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/injections.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/rainbow.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/scripts.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/gui/types.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/py.typed +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/speedhack.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/tts.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/vendor/FASM.EXE +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/vendor/big_logo.png +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/vendor/speedhack32.dll +0 -0
- {trainerbase-4.8.1 → trainerbase-4.9.0}/trainerbase/vendor/speedhack64.dll +0 -0
- {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.
|
|
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",
|
|
@@ -34,20 +34,27 @@ src = ["trainerbase"]
|
|
|
34
34
|
line-length = 120
|
|
35
35
|
|
|
36
36
|
[tool.ruff.lint]
|
|
37
|
-
select = [
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
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>=
|
|
79
|
-
"pyright>=1.1.
|
|
80
|
-
"ruff>=0.
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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,58 +110,69 @@ 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(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
|
-
|
|
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):
|
|
151
171
|
if self.new_memory_address and self.injection_point:
|
|
152
|
-
logger.debug(
|
|
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(
|
|
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
|
-
|
|
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
|
|
|
@@ -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(
|
|
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
|
|
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,11 +19,11 @@ 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
|
-
if offsets:
|
|
26
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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)
|
|
@@ -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)
|
|
@@ -186,11 +186,11 @@ class GameBool(GameObject[bool, bool], Switchable):
|
|
|
186
186
|
|
|
187
187
|
@override
|
|
188
188
|
def enable(self):
|
|
189
|
-
self.value = True
|
|
189
|
+
self.value = True
|
|
190
190
|
|
|
191
191
|
@override
|
|
192
192
|
def disable(self):
|
|
193
|
-
self.value = False
|
|
193
|
+
self.value = False
|
|
194
194
|
|
|
195
195
|
|
|
196
196
|
class ReadonlyGameObjectSumGetter(AbstractReadableObject[int | float]):
|
|
@@ -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.
|
|
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])
|
|
134
|
-
max_value = min(max_value, GameInt.value_range[1])
|
|
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.
|
|
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
|
|
|
@@ -38,7 +38,7 @@ getLogger("comtypes").setLevel(ERROR)
|
|
|
38
38
|
|
|
39
39
|
logger = getLogger("TrainerBase")
|
|
40
40
|
if using_default_config:
|
|
41
|
-
logger.debug(
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
186
|
+
assert_never(obj)
|
|
178
187
|
|
|
179
188
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
77
|
-
|
|
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
|
|
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
|