trainerbase 4.8.1__tar.gz → 4.8.2__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.8.2}/PKG-INFO +1 -1
- {trainerbase-4.8.1 → trainerbase-4.8.2}/pyproject.toml +20 -15
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/codeinjection.py +8 -8
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/common/helpers.py +4 -3
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/common/rainbow.py +1 -2
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/config.py +1 -1
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/gameobject.py +2 -2
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/gui/objects.py +6 -4
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/logger.py +1 -1
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/main.py +1 -2
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/memory.py +17 -12
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/process.py +2 -2
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/scriptengine.py +3 -3
- {trainerbase-4.8.1 → trainerbase-4.8.2}/README.md +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/__init__.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/abc.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/common/__init__.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/common/abc.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/common/keyboard.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/common/tapper.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/common/teleport.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/gui/__init__.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/gui/helpers.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/gui/injections.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/gui/misc.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/gui/rainbow.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/gui/scripts.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/gui/speedhack.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/gui/tapper.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/gui/teleport.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/gui/types.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/py.typed +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/speedhack.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/tts.py +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/vendor/FASM.EXE +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/vendor/big_logo.png +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/vendor/speedhack32.dll +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/vendor/speedhack64.dll +0 -0
- {trainerbase-4.8.1 → trainerbase-4.8.2}/trainerbase/vendor/trainerbase.ico +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "trainerbase"
|
|
3
|
-
version = "4.8.
|
|
3
|
+
version = "4.8.2"
|
|
4
4
|
description = "Framework for creating game trainers/mods"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
authors = [
|
|
@@ -34,20 +34,25 @@ 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",
|
|
51
56
|
]
|
|
52
57
|
|
|
53
58
|
[tool.ruff.lint.isort]
|
|
@@ -59,7 +59,7 @@ class CodeInjection(AbstractCodeInjection):
|
|
|
59
59
|
self.injection_point = self.address.resolve()
|
|
60
60
|
self.original_code = pm.read_bytes(self.injection_point, len(self.code_to_inject))
|
|
61
61
|
|
|
62
|
-
logger.debug(
|
|
62
|
+
logger.debug("Injecting %d byte(s) at 0x%X", len(self.code_to_inject), self.injection_point)
|
|
63
63
|
|
|
64
64
|
pm.write_bytes(self.injection_point, self.code_to_inject, len(self.code_to_inject))
|
|
65
65
|
self._injected = True
|
|
@@ -67,7 +67,7 @@ class CodeInjection(AbstractCodeInjection):
|
|
|
67
67
|
@override
|
|
68
68
|
def eject(self):
|
|
69
69
|
if self._injected:
|
|
70
|
-
logger.debug(
|
|
70
|
+
logger.debug("Ejecting %d byte(s) from 0x%X", len(self.code_to_inject), self.injection_point)
|
|
71
71
|
|
|
72
72
|
pm.write_bytes(self.injection_point, self.original_code, len(self.original_code))
|
|
73
73
|
self._injected = False
|
|
@@ -140,19 +140,19 @@ class AllocatingCodeInjection(AbstractCodeInjection):
|
|
|
140
140
|
|
|
141
141
|
code_to_inject = code_to_inject + jump_back
|
|
142
142
|
|
|
143
|
-
logger.debug(
|
|
143
|
+
logger.debug("Injecting %d byte(s) (INJECTION) at 0x%X", len(code_to_inject), self.new_memory_address)
|
|
144
144
|
pm.write_bytes(self.new_memory_address, code_to_inject, len(code_to_inject))
|
|
145
145
|
|
|
146
|
-
logger.debug(
|
|
146
|
+
logger.debug("Injecting %d byte(s) (JUMPER) at 0x%X", len(jump_to_new_mem), self.injection_point)
|
|
147
147
|
pm.write_bytes(self.injection_point, jump_to_new_mem, len(jump_to_new_mem))
|
|
148
148
|
|
|
149
149
|
@override
|
|
150
150
|
def eject(self):
|
|
151
151
|
if self.new_memory_address and self.injection_point:
|
|
152
|
-
logger.debug(
|
|
152
|
+
logger.debug("Ejecting %d byte(s) (JUMPER) from 0x%X", self.original_code_length, self.injection_point)
|
|
153
153
|
pm.write_bytes(self.injection_point, self.original_code, self.original_code_length)
|
|
154
154
|
|
|
155
|
-
logger.debug(
|
|
155
|
+
logger.debug("Freeing memory at 0x%X", self.new_memory_address)
|
|
156
156
|
pm.free(self.new_memory_address)
|
|
157
157
|
self.new_memory_address = 0
|
|
158
158
|
self.injection_point = 0
|
|
@@ -200,7 +200,7 @@ def preprocess_asm(code: str, *, original_code: bytes | None, base_address: int
|
|
|
200
200
|
@lru_cache
|
|
201
201
|
def compile_asm(code: Code) -> bytes:
|
|
202
202
|
if isinstance(code, str):
|
|
203
|
-
logger.debug(
|
|
203
|
+
logger.debug("Assembling for x%d:\n%s", ARCH, code)
|
|
204
204
|
|
|
205
205
|
fasm_mode = f"use{ARCH}"
|
|
206
206
|
with TemporaryDirectory() as tmp_dir:
|
|
@@ -208,7 +208,7 @@ def compile_asm(code: Code) -> bytes:
|
|
|
208
208
|
|
|
209
209
|
asm_file.write_text(f"{fasm_mode}\n{code}", encoding="utf-8")
|
|
210
210
|
|
|
211
|
-
run_executable([FASM_EXECUTABLE_PATH, asm_file], stdout=DEVNULL, check=True)
|
|
211
|
+
run_executable([FASM_EXECUTABLE_PATH, asm_file], stdout=DEVNULL, check=True) # noqa: S603
|
|
212
212
|
code = asm_file.with_suffix(".bin").read_bytes()
|
|
213
213
|
|
|
214
214
|
if not isinstance(code, bytes):
|
|
@@ -22,8 +22,8 @@ class ASMArrayManager(Sequence):
|
|
|
22
22
|
offsets: None | list[int] = 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
|
|
@@ -72,7 +72,8 @@ class ASMArrayManager(Sequence):
|
|
|
72
72
|
if from_register == array_index_register:
|
|
73
73
|
array_index_register = reserve_register
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
if len({from_register, array_address_register, array_index_register}) != 3:
|
|
76
|
+
raise RuntimeError("Registers must be unique")
|
|
76
77
|
|
|
77
78
|
label_continue = f"__label_continue_{uuid4().hex}"
|
|
78
79
|
|
|
@@ -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)
|
|
@@ -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]):
|
|
@@ -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():
|
|
@@ -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()
|
|
@@ -15,7 +15,8 @@ from trainerbase.process import pm
|
|
|
15
15
|
|
|
16
16
|
ARCH: Final[int] = 64 if pm.is_64_bit else 32
|
|
17
17
|
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)
|
|
18
|
+
DEFAULT_HARDCODED_ADDRESS_OFFSET_READER: Final[Callable[[int], int]] = cast(Callable[[int], int], pm.read_uint)
|
|
19
|
+
ANY_BYTE_MARKER_HEX: Final[str] = "2E" # 2E is hex(46), 46 is ord(".")
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
type ConvertibleToAddress = AbstractAddress | int | bytes | str
|
|
@@ -24,7 +25,7 @@ type ConvertibleToAddress = AbstractAddress | int | bytes | str
|
|
|
24
25
|
read_pointer = cast(Callable[[int], int], pm.read_ulonglong if pm.is_64_bit else pm.read_uint)
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
logger.debug(
|
|
28
|
+
logger.debug("Process arch is %d", ARCH)
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
class AbstractAddress(ABC):
|
|
@@ -51,9 +52,10 @@ class AOBScan(AbstractAddress):
|
|
|
51
52
|
):
|
|
52
53
|
self._should_cache = should_cache
|
|
53
54
|
self._saved_scan_result: int | None = None
|
|
55
|
+
self._any_byte_marker = any_byte_marker
|
|
54
56
|
|
|
55
57
|
if isinstance(aob, str):
|
|
56
|
-
aob = bytes.fromhex(aob.replace(any_byte_marker,
|
|
58
|
+
aob = bytes.fromhex(aob.replace(any_byte_marker, ANY_BYTE_MARKER_HEX)) # 2E is hex(46), 46 is ord(".")
|
|
57
59
|
|
|
58
60
|
if should_escape:
|
|
59
61
|
aob = self._escape_aob(aob)
|
|
@@ -88,7 +90,8 @@ class AOBScan(AbstractAddress):
|
|
|
88
90
|
scan_result: list[int] | int | None = find_aob(self.aob, check_memory_protection=self.check_memory_protection)
|
|
89
91
|
|
|
90
92
|
if not scan_result:
|
|
91
|
-
|
|
93
|
+
aob_repr = self.aob.hex(" ").upper().replace(ANY_BYTE_MARKER_HEX, self._any_byte_marker)
|
|
94
|
+
raise ValueError(f"AOB not found: {aob_repr}")
|
|
92
95
|
|
|
93
96
|
if isinstance(scan_result, list):
|
|
94
97
|
result_address = scan_result[cast(int, self.multiple_result_index)]
|
|
@@ -174,12 +177,12 @@ def ensure_address(obj: ConvertibleToAddress) -> AbstractAddress:
|
|
|
174
177
|
case bytes() | str():
|
|
175
178
|
return AOBScan(obj)
|
|
176
179
|
case _:
|
|
177
|
-
|
|
180
|
+
assert_never(obj)
|
|
178
181
|
|
|
179
182
|
|
|
180
183
|
def allocate(size: int = POINTER_SIZE, count: int = 1) -> int:
|
|
181
184
|
total = size * count
|
|
182
|
-
logger.debug(
|
|
185
|
+
logger.debug("Allocating %d bytes.", total)
|
|
183
186
|
|
|
184
187
|
return pm.allocate(total)
|
|
185
188
|
|
|
@@ -239,19 +242,19 @@ def get_module_address(module_name: str, *, delay: float = 2, retry_limit: int |
|
|
|
239
242
|
Set retry_limit to 0 if you want to disable retries.
|
|
240
243
|
"""
|
|
241
244
|
|
|
242
|
-
logger.info(
|
|
245
|
+
logger.info("Looking for %s...", module_name)
|
|
243
246
|
|
|
244
247
|
for try_counter in count():
|
|
245
248
|
module_info = module_from_name(pm.process_handle, module_name)
|
|
246
249
|
|
|
247
250
|
if module_info is not None:
|
|
248
|
-
logger.info(
|
|
251
|
+
logger.info("Successfully found %s at 0x%X", module_name, module_info.lpBaseOfDll)
|
|
249
252
|
return module_info.lpBaseOfDll
|
|
250
253
|
|
|
251
254
|
if try_counter == retry_limit:
|
|
252
255
|
raise RuntimeError(f"Failed to find module: {module_name}, tried {try_counter + 1} time(s).")
|
|
253
256
|
|
|
254
|
-
logger.info(
|
|
257
|
+
logger.info("Failed to find %s. I will retry in %f s. Retry limit is %s", module_name, delay, retry_limit)
|
|
255
258
|
|
|
256
259
|
sleep(delay)
|
|
257
260
|
|
|
@@ -273,7 +276,7 @@ def ensure_resolved(
|
|
|
273
276
|
Set retry_limit to 0 if you want to disable retries.
|
|
274
277
|
"""
|
|
275
278
|
|
|
276
|
-
logger.info(
|
|
279
|
+
logger.info("Looking for %s...", verbose_name)
|
|
277
280
|
|
|
278
281
|
for try_counter in count():
|
|
279
282
|
try:
|
|
@@ -282,12 +285,14 @@ def ensure_resolved(
|
|
|
282
285
|
if try_counter == retry_limit:
|
|
283
286
|
raise RuntimeError(f"Failed to resolve {verbose_name}, tried {try_counter + 1} time(s).") from e
|
|
284
287
|
|
|
285
|
-
logger.info(
|
|
288
|
+
logger.info(
|
|
289
|
+
"Failed to resolve %s. I will retry in %f s. Retry limit is %s", verbose_name, delay, retry_limit
|
|
290
|
+
)
|
|
286
291
|
|
|
287
292
|
sleep(delay)
|
|
288
293
|
continue
|
|
289
294
|
|
|
290
|
-
logger.info(
|
|
295
|
+
logger.info("Successfully found %s at 0x%X", verbose_name, resolved)
|
|
291
296
|
return resolved
|
|
292
297
|
|
|
293
298
|
raise RuntimeError("This code must be unreachable!")
|
|
@@ -46,14 +46,14 @@ 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
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from collections.abc import Callable
|
|
3
3
|
from contextlib import suppress
|
|
4
|
-
from sys import stderr
|
|
5
4
|
from threading import Thread
|
|
6
5
|
from time import sleep
|
|
7
6
|
from typing import Any, override
|
|
@@ -9,6 +8,7 @@ from typing import Any, override
|
|
|
9
8
|
from pymem.exception import MemoryReadError, MemoryWriteError
|
|
10
9
|
|
|
11
10
|
from trainerbase.abc import Switchable
|
|
11
|
+
from trainerbase.logger import logger
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class AbstractBaseScript(Switchable, ABC):
|
|
@@ -73,8 +73,8 @@ class ScriptEngine:
|
|
|
73
73
|
script()
|
|
74
74
|
except MemoryReadError, MemoryWriteError:
|
|
75
75
|
continue
|
|
76
|
-
except Exception
|
|
77
|
-
|
|
76
|
+
except Exception:
|
|
77
|
+
logger.exception("Error in script %r. Stopping Script Engine.", script)
|
|
78
78
|
self.stop()
|
|
79
79
|
|
|
80
80
|
sleep(self.delay)
|
|
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
|
|
File without changes
|