ExternalAPI 0.1.0__py3-none-any.whl
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.
- ExternalAPI/__init__.py +11 -0
- ExternalAPI/include/Api/__init__.py +5 -0
- ExternalAPI/include/Api/manager.py +12 -0
- ExternalAPI/include/Api/parser.py +113 -0
- ExternalAPI/include/__init__.py +5 -0
- ExternalAPI/include/core/__init__.py +21 -0
- ExternalAPI/include/core/edit.py +42 -0
- ExternalAPI/include/core/get.py +39 -0
- ExternalAPI/include/core/inject.py +16 -0
- ExternalAPI/include/core/manager.py +60 -0
- ExternalAPI/include/core/memory.py +55 -0
- ExternalAPI/include/core/pars.py +54 -0
- ExternalAPI/include/validation.py +143 -0
- externalapi-0.1.0.dist-info/METADATA +78 -0
- externalapi-0.1.0.dist-info/RECORD +18 -0
- externalapi-0.1.0.dist-info/WHEEL +5 -0
- externalapi-0.1.0.dist-info/licenses/LICENSE +7 -0
- externalapi-0.1.0.dist-info/top_level.txt +1 -0
ExternalAPI/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .include.core import inject
|
|
2
|
+
from .include.core.get import get
|
|
3
|
+
from .include.core.edit import edit
|
|
4
|
+
from .include.core.pars import pars
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def init(process_name: str = "cs2.exe"):
|
|
8
|
+
return inject.init(process_name)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = ["init", "inject", "get", "edit", "pars"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from .parser import get_address_by_name
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ApiManager:
|
|
7
|
+
|
|
8
|
+
def __init__(self, offsets_path: Optional[str] = None) -> None:
|
|
9
|
+
self._offsets_path = offsets_path
|
|
10
|
+
|
|
11
|
+
def get_address(self, name: str) -> int:
|
|
12
|
+
return get_address_by_name(name, self._offsets_path)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Dict, Tuple, Optional
|
|
4
|
+
|
|
5
|
+
GITHUB_USER = "EclipseCS2"
|
|
6
|
+
REPO_NAME = "ExternalAPI"
|
|
7
|
+
RAW_URL = f"https://raw.githubusercontent.com/EclipseCS2/ExternalAPI/refs/heads/main/ExternalAPI/offsets.cfg"
|
|
8
|
+
|
|
9
|
+
def _default_offsets_path() -> Path:
|
|
10
|
+
"""Определяет путь к файлу offsets.cfg в корне проекта."""
|
|
11
|
+
return Path(__file__).resolve().parents[2] / "offsets.cfg"
|
|
12
|
+
|
|
13
|
+
def download_offsets(path: Path) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Скачивает свежий файл офсетов с GitHub и сохраняет его локально.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
print(f"[API] Checking for updates at: {RAW_URL}")
|
|
19
|
+
response = requests.get(RAW_URL, timeout=10)
|
|
20
|
+
if response.status_code == 200:
|
|
21
|
+
path.write_text(response.text, encoding="utf-8")
|
|
22
|
+
print(f"[API] Success! Offsets saved to {path.name}")
|
|
23
|
+
return True
|
|
24
|
+
else:
|
|
25
|
+
print(f"[API] Failed to download: Status {response.status_code}")
|
|
26
|
+
return False
|
|
27
|
+
except Exception as e:
|
|
28
|
+
print(f"[API] Network error during update: {e}")
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def parse_offsets(path: str | None = None) -> Dict[str, int]:
|
|
32
|
+
"""Парсит файл офсетов и возвращает словарь {имя: адрес}."""
|
|
33
|
+
cfg_path = Path(path) if path is not None else _default_offsets_path()
|
|
34
|
+
offsets: Dict[str, int] = {}
|
|
35
|
+
|
|
36
|
+
if not cfg_path.is_file():
|
|
37
|
+
if not download_offsets(cfg_path):
|
|
38
|
+
return offsets
|
|
39
|
+
|
|
40
|
+
text = cfg_path.read_text(encoding="utf-8", errors="ignore")
|
|
41
|
+
for raw_line in text.splitlines():
|
|
42
|
+
line = raw_line.strip()
|
|
43
|
+
if not line or line.startswith("#"):
|
|
44
|
+
continue
|
|
45
|
+
if "=" not in line:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
name, value = line.split("=", 1)
|
|
49
|
+
name = name.strip()
|
|
50
|
+
value = value.strip().strip("'\"")
|
|
51
|
+
|
|
52
|
+
if not name or not value:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
addr = int(value, 0)
|
|
57
|
+
offsets[name] = addr
|
|
58
|
+
except ValueError:
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
return offsets
|
|
62
|
+
|
|
63
|
+
def parse_offsets_with_modules(path: str | None = None) -> Dict[str, Tuple[int, Optional[str]]]:
|
|
64
|
+
"""Парсит офсеты и привязывает их к модулям (DLL)."""
|
|
65
|
+
cfg_path = Path(path) if path is not None else _default_offsets_path()
|
|
66
|
+
result: Dict[str, Tuple[int, Optional[str]]] = {}
|
|
67
|
+
current_module: Optional[str] = None
|
|
68
|
+
|
|
69
|
+
if not cfg_path.is_file():
|
|
70
|
+
if not download_offsets(cfg_path):
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
text = cfg_path.read_text(encoding="utf-8", errors="ignore")
|
|
74
|
+
for raw_line in text.splitlines():
|
|
75
|
+
line = raw_line.strip()
|
|
76
|
+
if not line:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if line.startswith("#"):
|
|
80
|
+
# Извлекаем имя модуля из комментария, например: "# client.dll"
|
|
81
|
+
potential_module = line.lstrip("#").strip()
|
|
82
|
+
if potential_module.lower().endswith(".dll"):
|
|
83
|
+
current_module = potential_module
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if "=" not in line:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
name, value = line.split("=", 1)
|
|
90
|
+
name = name.strip()
|
|
91
|
+
value = value.strip().strip("'\"")
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
addr = int(value, 0)
|
|
95
|
+
result[name] = (addr, current_module)
|
|
96
|
+
except ValueError:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
def get_address_by_name(name: str, path: str | None = None) -> int:
|
|
102
|
+
"""Возвращает адрес по его имени. Выбрасывает ошибку, если имя не найдено."""
|
|
103
|
+
offsets = parse_offsets(path)
|
|
104
|
+
if name not in offsets:
|
|
105
|
+
raise KeyError(f"Offset '{name}' not found in offsets.cfg")
|
|
106
|
+
return offsets[name]
|
|
107
|
+
|
|
108
|
+
def get_address_and_module(name: str, path: str | None = None) -> Tuple[int, Optional[str]]:
|
|
109
|
+
"""Возвращает кортеж (адрес, имя_модуля) по имени офсета."""
|
|
110
|
+
offsets = parse_offsets_with_modules(path)
|
|
111
|
+
if name not in offsets:
|
|
112
|
+
raise KeyError(f"Offset '{name}' not found in offsets.cfg")
|
|
113
|
+
return offsets[name]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .memory import Memory
|
|
2
|
+
from .manager import CoreManager, set_default_process, get_default_process
|
|
3
|
+
from .inject import init as inject_init
|
|
4
|
+
from .get import get, get_value
|
|
5
|
+
from .edit import edit, set_value
|
|
6
|
+
from .pars import pars, get_offset
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Memory",
|
|
10
|
+
"CoreManager",
|
|
11
|
+
"set_default_process",
|
|
12
|
+
"get_default_process",
|
|
13
|
+
"inject_init",
|
|
14
|
+
"get",
|
|
15
|
+
"get_value",
|
|
16
|
+
"edit",
|
|
17
|
+
"set_value",
|
|
18
|
+
"pars",
|
|
19
|
+
"get_offset",
|
|
20
|
+
]
|
|
21
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Optional, Union
|
|
2
|
+
|
|
3
|
+
from .manager import CoreManager
|
|
4
|
+
from ..validation import normalize_address
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def set_value(target: Union[int, str], value: int | float, process: int | str | None = None, offsets_path: Optional[str] = None) -> int:
|
|
8
|
+
core = CoreManager(process, offsets_path=offsets_path)
|
|
9
|
+
|
|
10
|
+
if isinstance(target, str):
|
|
11
|
+
addr = core.get_address(target)
|
|
12
|
+
else:
|
|
13
|
+
addr = normalize_address(target)
|
|
14
|
+
|
|
15
|
+
if isinstance(value, float):
|
|
16
|
+
core.memory.write_float(addr, value)
|
|
17
|
+
else:
|
|
18
|
+
core.memory.write_int(addr, int(value))
|
|
19
|
+
|
|
20
|
+
return addr
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _Editor:
|
|
24
|
+
def __call__(
|
|
25
|
+
self,
|
|
26
|
+
target: Union[int, str],
|
|
27
|
+
value: int | float,
|
|
28
|
+
*,
|
|
29
|
+
process: int | str | None = None,
|
|
30
|
+
offsets_path: Optional[str] = None,
|
|
31
|
+
) -> int:
|
|
32
|
+
return set_value(target, value, process=process, offsets_path=offsets_path)
|
|
33
|
+
|
|
34
|
+
def __getattr__(self, name: str):
|
|
35
|
+
raise AttributeError(
|
|
36
|
+
f"Синтаксис edit.{name}(...) отключён. Используйте edit('{name}', ...)"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
edit = _Editor()
|
|
41
|
+
|
|
42
|
+
__all__ = ["set_value", "edit"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Optional, Union
|
|
2
|
+
|
|
3
|
+
from .manager import CoreManager
|
|
4
|
+
from ..validation import normalize_address
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_value(target: Union[int, str], process: int | str | None = None, as_float: bool = False, offsets_path: Optional[str] = None) -> int | float:
|
|
8
|
+
core = CoreManager(process, offsets_path=offsets_path)
|
|
9
|
+
|
|
10
|
+
if isinstance(target, str):
|
|
11
|
+
addr = core.get_address(target)
|
|
12
|
+
else:
|
|
13
|
+
addr = normalize_address(target)
|
|
14
|
+
|
|
15
|
+
if as_float:
|
|
16
|
+
return core.memory.read_float(addr)
|
|
17
|
+
return core.memory.read_int(addr)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _Getter:
|
|
21
|
+
def __call__(
|
|
22
|
+
self,
|
|
23
|
+
target: Union[int, str],
|
|
24
|
+
*,
|
|
25
|
+
process: int | str | None = None,
|
|
26
|
+
as_float: bool = False,
|
|
27
|
+
offsets_path: Optional[str] = None,
|
|
28
|
+
) -> int | float:
|
|
29
|
+
return get_value(target, process=process, as_float=as_float, offsets_path=offsets_path)
|
|
30
|
+
|
|
31
|
+
def __getattr__(self, name: str):
|
|
32
|
+
raise AttributeError(
|
|
33
|
+
f"Синтаксис get.{name}() отключён. Используйте get('{name}')"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
get = _Getter()
|
|
38
|
+
|
|
39
|
+
__all__ = ["get_value", "get"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import pymem
|
|
2
|
+
import pymem.process
|
|
3
|
+
|
|
4
|
+
from .memory import Memory
|
|
5
|
+
from .manager import set_default_process
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def init(process_name: str = "cs2.exe") -> Memory:
|
|
9
|
+
try:
|
|
10
|
+
proc_entry = pymem.process.process_from_name(process_name)
|
|
11
|
+
except ProcessLookupError:
|
|
12
|
+
raise RuntimeError(f"Process '{process_name}' not found")
|
|
13
|
+
|
|
14
|
+
pid = proc_entry.th32ProcessID
|
|
15
|
+
set_default_process(pid)
|
|
16
|
+
return Memory(pid)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from ..Api.manager import ApiManager
|
|
4
|
+
from ..Api.parser import get_address_and_module
|
|
5
|
+
from .memory import Memory
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_default_process: int | str | None = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def set_default_process(process: int | str) -> None:
|
|
12
|
+
global _default_process
|
|
13
|
+
_default_process = process
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_default_process() -> int | str:
|
|
17
|
+
if _default_process is None:
|
|
18
|
+
raise RuntimeError("Default process is not set.")
|
|
19
|
+
return _default_process
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CoreManager:
|
|
23
|
+
def __init__(self, process: int | str | None = None, offsets_path: Optional[str] = None) -> None:
|
|
24
|
+
if process is None:
|
|
25
|
+
process = get_default_process()
|
|
26
|
+
|
|
27
|
+
self.api_manager = ApiManager(offsets_path)
|
|
28
|
+
self._offsets_path = offsets_path
|
|
29
|
+
self.memory = Memory(process)
|
|
30
|
+
|
|
31
|
+
def get_address(self, name: str) -> int:
|
|
32
|
+
offset = self.api_manager.get_address(name)
|
|
33
|
+
|
|
34
|
+
needs_base = name.startswith("dw") or name in [
|
|
35
|
+
"attack",
|
|
36
|
+
"attack2",
|
|
37
|
+
"back",
|
|
38
|
+
"duck",
|
|
39
|
+
"forward",
|
|
40
|
+
"jump",
|
|
41
|
+
"left",
|
|
42
|
+
"right",
|
|
43
|
+
"use",
|
|
44
|
+
"reload",
|
|
45
|
+
"sprint",
|
|
46
|
+
"lookatweapon",
|
|
47
|
+
"showscores",
|
|
48
|
+
"turnleft",
|
|
49
|
+
"turnright",
|
|
50
|
+
"zoom",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
if not needs_base:
|
|
54
|
+
return offset
|
|
55
|
+
|
|
56
|
+
_, module_name = get_address_and_module(name, self._offsets_path)
|
|
57
|
+
dll_name = module_name or "client.dll"
|
|
58
|
+
|
|
59
|
+
base = self.memory.get_module_base(dll_name)
|
|
60
|
+
return base + offset
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import pymem
|
|
2
|
+
import pymem.process
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Memory:
|
|
6
|
+
|
|
7
|
+
def __init__(self, process: int | str) -> None:
|
|
8
|
+
if isinstance(process, int):
|
|
9
|
+
pm = pymem.Pymem()
|
|
10
|
+
pm.open_process_from_id(process)
|
|
11
|
+
else:
|
|
12
|
+
pm = pymem.Pymem(process)
|
|
13
|
+
|
|
14
|
+
self._pm = pm
|
|
15
|
+
|
|
16
|
+
def get_module_base(self, module_name: str) -> int:
|
|
17
|
+
module = pymem.process.module_from_name(self._pm.process_handle, module_name)
|
|
18
|
+
if not module:
|
|
19
|
+
return 0
|
|
20
|
+
return module.lpBaseOfDll
|
|
21
|
+
|
|
22
|
+
# --- указатели (8 байт, x64) ---
|
|
23
|
+
def read_longlong(self, address: int) -> int:
|
|
24
|
+
return self._pm.read_longlong(address)
|
|
25
|
+
|
|
26
|
+
# --- базовые байты ---
|
|
27
|
+
def read_bytes(self, address: int, size: int) -> bytes:
|
|
28
|
+
return self._pm.read_bytes(address, size)
|
|
29
|
+
|
|
30
|
+
def write_bytes(self, address: int, data: bytes | bytearray) -> None:
|
|
31
|
+
self._pm.write_bytes(address, data)
|
|
32
|
+
|
|
33
|
+
# --- int (4 байта) ---
|
|
34
|
+
def read_int(self, address: int) -> int:
|
|
35
|
+
return self._pm.read_int(address)
|
|
36
|
+
|
|
37
|
+
def write_int(self, address: int, value: int) -> None:
|
|
38
|
+
self._pm.write_int(address, int(value))
|
|
39
|
+
|
|
40
|
+
# --- float (4 байта) ---
|
|
41
|
+
def read_float(self, address: int) -> float:
|
|
42
|
+
return self._pm.read_float(address)
|
|
43
|
+
|
|
44
|
+
def write_float(self, address: int, value: float) -> None:
|
|
45
|
+
self._pm.write_float(address, float(value))
|
|
46
|
+
|
|
47
|
+
# --- double (8 байт) ---
|
|
48
|
+
def read_double(self, address: int) -> float:
|
|
49
|
+
return self._pm.read_double(address)
|
|
50
|
+
|
|
51
|
+
def write_double(self, address: int, value: float) -> None:
|
|
52
|
+
self._pm.write_double(address, float(value))
|
|
53
|
+
|
|
54
|
+
def close(self) -> None:
|
|
55
|
+
self._pm.close_process()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from ..Api.manager import ApiManager
|
|
4
|
+
from ..validation import normalize_offset
|
|
5
|
+
from .manager import CoreManager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_offset(name: str, offsets_path: Optional[str] = None) -> int:
|
|
9
|
+
api = ApiManager(offsets_path)
|
|
10
|
+
raw = normalize_offset(api.get_address(name))
|
|
11
|
+
|
|
12
|
+
if not (name.startswith("dw") or name in [
|
|
13
|
+
"attack",
|
|
14
|
+
"attack2",
|
|
15
|
+
"back",
|
|
16
|
+
"duck",
|
|
17
|
+
"forward",
|
|
18
|
+
"jump",
|
|
19
|
+
"left",
|
|
20
|
+
"right",
|
|
21
|
+
"use",
|
|
22
|
+
"reload",
|
|
23
|
+
"sprint",
|
|
24
|
+
"lookatweapon",
|
|
25
|
+
"showscores",
|
|
26
|
+
"turnleft",
|
|
27
|
+
"turnright",
|
|
28
|
+
"zoom",
|
|
29
|
+
]):
|
|
30
|
+
return raw
|
|
31
|
+
|
|
32
|
+
core = CoreManager(process=None, offsets_path=offsets_path)
|
|
33
|
+
|
|
34
|
+
if name == "dwLocalPlayerPawn":
|
|
35
|
+
ptr_addr = core.get_address(name) # base + offset (указатель)
|
|
36
|
+
return core.memory.read_longlong(ptr_addr) # адрес pawn
|
|
37
|
+
|
|
38
|
+
return core.get_address(name)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _Parser:
|
|
42
|
+
def __call__(self, target: str, *, offsets_path: Optional[str] = None) -> int:
|
|
43
|
+
return get_offset(target, offsets_path=offsets_path)
|
|
44
|
+
|
|
45
|
+
def __getattr__(self, name: str):
|
|
46
|
+
raise AttributeError(
|
|
47
|
+
f"Синтаксис pars.{name}() отключён. Используйте pars('{name}')"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
pars = _Parser()
|
|
52
|
+
|
|
53
|
+
__all__ = ["get_offset", "pars"]
|
|
54
|
+
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from typing import Union, Iterable, Mapping, Any, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
NumberLike = Union[int, str]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def to_int(value: NumberLike) -> int:
|
|
8
|
+
if isinstance(value, int):
|
|
9
|
+
return value
|
|
10
|
+
if isinstance(value, str):
|
|
11
|
+
text = value.strip()
|
|
12
|
+
if not text:
|
|
13
|
+
raise ValueError("Empty string cannot be converted to int")
|
|
14
|
+
return int(text, 0)
|
|
15
|
+
raise TypeError(f"Unsupported type for to_int: {type(value)!r}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def to_float(value: Union[NumberLike, float]) -> float:
|
|
19
|
+
if isinstance(value, float):
|
|
20
|
+
return value
|
|
21
|
+
if isinstance(value, int):
|
|
22
|
+
return float(value)
|
|
23
|
+
if isinstance(value, str):
|
|
24
|
+
text = value.strip()
|
|
25
|
+
if not text:
|
|
26
|
+
raise ValueError("Empty string cannot be converted to float")
|
|
27
|
+
# Попробуем сначала как обычное число, потом как hex
|
|
28
|
+
try:
|
|
29
|
+
return float(text)
|
|
30
|
+
except ValueError:
|
|
31
|
+
return float(int(text, 0))
|
|
32
|
+
raise TypeError(f"Unsupported type for to_float: {type(value)!r}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def to_bool(value: Any) -> bool:
|
|
36
|
+
if isinstance(value, bool):
|
|
37
|
+
return value
|
|
38
|
+
if isinstance(value, int):
|
|
39
|
+
if value in (0, 1):
|
|
40
|
+
return bool(value)
|
|
41
|
+
raise ValueError(f"Cannot convert int {value!r} to bool (only 0 or 1 allowed)")
|
|
42
|
+
if isinstance(value, str):
|
|
43
|
+
text = value.strip().lower()
|
|
44
|
+
if text in ("", "0", "false", "no", "off"):
|
|
45
|
+
return False
|
|
46
|
+
if text in ("1", "true", "yes", "on"):
|
|
47
|
+
return True
|
|
48
|
+
raise ValueError(f"Cannot convert string {value!r} to bool")
|
|
49
|
+
raise TypeError(f"Unsupported type for to_bool: {type(value)!r}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def normalize_offset(value: NumberLike) -> int:
|
|
53
|
+
return to_int(value)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def normalize_address(value: NumberLike) -> int:
|
|
57
|
+
addr = to_int(value)
|
|
58
|
+
if addr < 0:
|
|
59
|
+
raise ValueError("Address must be non-negative")
|
|
60
|
+
hex_str = hex(addr)
|
|
61
|
+
return int(hex_str, 16)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def to_hex(value: NumberLike) -> str:
|
|
65
|
+
return hex(to_int(value))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def ensure_positive_int(value: NumberLike, *, allow_zero: bool = False) -> int:
|
|
69
|
+
iv = to_int(value)
|
|
70
|
+
if allow_zero:
|
|
71
|
+
if iv < 0:
|
|
72
|
+
raise ValueError(f"Value must be >= 0, got {iv}")
|
|
73
|
+
else:
|
|
74
|
+
if iv <= 0:
|
|
75
|
+
raise ValueError(f"Value must be > 0, got {iv}")
|
|
76
|
+
return iv
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def ensure_in_range(
|
|
80
|
+
value: NumberLike,
|
|
81
|
+
*,
|
|
82
|
+
min_value: Optional[int] = None,
|
|
83
|
+
max_value: Optional[int] = None,
|
|
84
|
+
) -> int:
|
|
85
|
+
iv = to_int(value)
|
|
86
|
+
if min_value is not None and iv < min_value:
|
|
87
|
+
raise ValueError(f"Value must be >= {min_value}, got {iv}")
|
|
88
|
+
if max_value is not None and iv > max_value:
|
|
89
|
+
raise ValueError(f"Value must be <= {max_value}, got {iv}")
|
|
90
|
+
return iv
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def ensure_non_empty_string(value: Any) -> str:
|
|
94
|
+
if not isinstance(value, str):
|
|
95
|
+
raise TypeError(f"Expected non-empty string, got {type(value)!r}")
|
|
96
|
+
text = value.strip()
|
|
97
|
+
if not text:
|
|
98
|
+
raise ValueError("String must be non-empty")
|
|
99
|
+
return text
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def ensure_iterable_not_empty(value: Iterable[Any], *, name: str = "value") -> Iterable[Any]:
|
|
103
|
+
try:
|
|
104
|
+
if len(value) == 0: # type: ignore[arg-type]
|
|
105
|
+
raise ValueError(f"{name} must not be empty")
|
|
106
|
+
return value
|
|
107
|
+
except TypeError:
|
|
108
|
+
items = list(value)
|
|
109
|
+
if not items:
|
|
110
|
+
raise ValueError(f"{name} must not be empty")
|
|
111
|
+
return items
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def ensure_mapping_has_keys(
|
|
115
|
+
mapping: Mapping[str, Any],
|
|
116
|
+
required_keys: Iterable[str],
|
|
117
|
+
*,
|
|
118
|
+
name: str = "mapping",
|
|
119
|
+
) -> Mapping[str, Any]:
|
|
120
|
+
"""
|
|
121
|
+
Проверка, что в словаре есть все требуемые ключи.
|
|
122
|
+
Удобно для настройки/конфигов.
|
|
123
|
+
"""
|
|
124
|
+
missing = [k for k in required_keys if k not in mapping]
|
|
125
|
+
if missing:
|
|
126
|
+
raise KeyError(f"{name} is missing required keys: {', '.join(missing)}")
|
|
127
|
+
return mapping
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__all__ = [
|
|
131
|
+
"to_int",
|
|
132
|
+
"to_float",
|
|
133
|
+
"to_bool",
|
|
134
|
+
"normalize_offset",
|
|
135
|
+
"normalize_address",
|
|
136
|
+
"to_hex",
|
|
137
|
+
"ensure_positive_int",
|
|
138
|
+
"ensure_in_range",
|
|
139
|
+
"ensure_non_empty_string",
|
|
140
|
+
"ensure_iterable_not_empty",
|
|
141
|
+
"ensure_mapping_has_keys",
|
|
142
|
+
]
|
|
143
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ExternalAPI
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ExternalAPI
|
|
5
|
+
Author: Eclipse
|
|
6
|
+
License: Copyright (c) 2026 Eclipse Hack
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
11
|
+
|
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# ExternalAPI Eclipse 🌑
|
|
23
|
+
|
|
24
|
+
**CS2 Eclipse** is a high-performance External Memory Interface (EMI) designed for Counter-Strike 2. This framework provides a clean abstraction layer over Source 2 engine offsets, enabling seamless interaction with game memory for research and development purposes.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🌑 Installation & Dependencies
|
|
29
|
+
|
|
30
|
+
The project relies on low-level libraries to handle process memory and Windows API calls. To set up your environment, ensure you have Python installed and run the following command:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python -m pip install -r requirements.txt
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
> **Core Dependencies:** `Pymem`, `pywin32`.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🌑 Technical Specifications (Core Offsets)
|
|
42
|
+
|
|
43
|
+
The API is architected to support all critical memory addresses, categorized by their function within the game engine:
|
|
44
|
+
|
|
45
|
+
### 1. Global & Engine Access
|
|
46
|
+
|
|
47
|
+
Primary entry points within `client.dll`:
|
|
48
|
+
|
|
49
|
+
* **`dwEntitySystem` / `dwEntityList**` — Global entity registry management.
|
|
50
|
+
* **`dwLocalPlayerController`** — Local player metadata and network state.
|
|
51
|
+
* **`dwViewMatrix`** — $4 \times 4$ transformation matrix for World-to-Screen calculations.
|
|
52
|
+
* **`dwGameRules`** — Internal match state and round parameters.
|
|
53
|
+
|
|
54
|
+
### 2. Player Pawn & Data Members
|
|
55
|
+
|
|
56
|
+
Specific offsets for real-time entity state monitoring:
|
|
57
|
+
|
|
58
|
+
* **Combat:** `m_iHealth`, `m_ArmorValue`, `m_iShotsFired`, `m_aimPunchAngle`.
|
|
59
|
+
* **Movement:** `m_vecVelocity`, `m_fFlags`, `m_vOldOrigin`.
|
|
60
|
+
* **Status:** `m_bIsScoped`, `m_bIsDefusing`, `m_flFlashDuration`, `m_lifeState`.
|
|
61
|
+
|
|
62
|
+
### 3. World & Objects
|
|
63
|
+
|
|
64
|
+
Environment-specific offsets:
|
|
65
|
+
|
|
66
|
+
* **C4 Dynamics:** `dwPlantedC4`, `m_flC4Blow`, `m_bBombPlanted`.
|
|
67
|
+
* **Equipment:** `m_pClippingWeapon`, `m_iItemDefinitionIndex`, `m_iClip1`.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## ⚠️ Disclaimer
|
|
72
|
+
|
|
73
|
+
**Educational Purpose Only.** This framework is intended for reverse engineering and software architecture research.
|
|
74
|
+
|
|
75
|
+
* The developer is not responsible for any account restrictions or bans.
|
|
76
|
+
* Usage on VAC-secured (Valve Anti-Cheat) servers is strictly discouraged.
|
|
77
|
+
|
|
78
|
+
---
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
ExternalAPI/__init__.py,sha256=rYH6J-VFJm96mvEhZLtjAIYTj_G4iqGZPxWLAHhDtkQ,273
|
|
2
|
+
ExternalAPI/include/__init__.py,sha256=ELDjKIwCqc3_cLLzIAJiyO-LUKkXSN_I0Or5DzTaD2g,70
|
|
3
|
+
ExternalAPI/include/validation.py,sha256=W7IORWKrRCAcqOOlVRKyRpEbXYx8BX_wefJaFL5vaLU,4301
|
|
4
|
+
ExternalAPI/include/Api/__init__.py,sha256=8dYrp6rpKKh47LfLrU_0-wwGn7Dg9pWrSqyS_9mYNhg,159
|
|
5
|
+
ExternalAPI/include/Api/manager.py,sha256=dVfdwW2RrLJh40BRE1dgeG7IcypNa4IYQzB-xk2r97s,319
|
|
6
|
+
ExternalAPI/include/Api/parser.py,sha256=9OScbvN0PHipJrgc7f4a_Nu_hUFapGGu6AW52XrvHxI,4249
|
|
7
|
+
ExternalAPI/include/core/__init__.py,sha256=TLQ3tiIQ7VztuEJ7J_jXMzJ40GUFe9fkSZDKx8fBcHk,473
|
|
8
|
+
ExternalAPI/include/core/edit.py,sha256=LBQ_bRkiPB4yhnCewNu4s1A7iJgoEPpI8aAh1qNPO20,1178
|
|
9
|
+
ExternalAPI/include/core/get.py,sha256=AHbMoFJRnk88PqaZwIWKv6F7kee7Ui45WwnzlCfzplM,1145
|
|
10
|
+
ExternalAPI/include/core/inject.py,sha256=hvIS1mMDIuEVPeVuTR3ij_qxiz0hSmEnu3ux15yrvJU,433
|
|
11
|
+
ExternalAPI/include/core/manager.py,sha256=YjyvZZRcPETuHbmMIft1kf7sCNGTbsqisgiojiBjgPk,1627
|
|
12
|
+
ExternalAPI/include/core/memory.py,sha256=eiyXZlZ9KHd8cmGs0bnwW3rENWS9AtqgwBDFjZiS4Ic,1771
|
|
13
|
+
ExternalAPI/include/core/pars.py,sha256=fI1iMtX2FfhEkd53E2T4RSay3Xbd0jHKO9ljG9rai-Y,1410
|
|
14
|
+
externalapi-0.1.0.dist-info/licenses/LICENSE,sha256=ICcRlT_5ebbybSxXoh6Mt6ynNrzEf4pkzFQzlIg_7Po,1061
|
|
15
|
+
externalapi-0.1.0.dist-info/METADATA,sha256=O2q7JRU-wXAR9dWt00-H1nq130CCnceKFnKHdcOTpeA,3399
|
|
16
|
+
externalapi-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
17
|
+
externalapi-0.1.0.dist-info/top_level.txt,sha256=QEUusLJlsEYFRHMum_SVhQs0g5bjy0VX110gK51WRpg,12
|
|
18
|
+
externalapi-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2026 Eclipse Hack
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ExternalAPI
|