robloxmemoryapi 0.0.1__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.
- robloxmemoryapi/__init__.py +87 -0
- robloxmemoryapi/data/offsets.json +6 -0
- robloxmemoryapi/utils/__init__.py +2 -0
- robloxmemoryapi/utils/memory.py +275 -0
- robloxmemoryapi/utils/offsets.py +46 -0
- robloxmemoryapi/utils/rbx/__init__.py +0 -0
- robloxmemoryapi/utils/rbx/datastructures.py +266 -0
- robloxmemoryapi/utils/rbx/instance.py +457 -0
- robloxmemoryapi-0.0.1.dist-info/METADATA +75 -0
- robloxmemoryapi-0.0.1.dist-info/RECORD +13 -0
- robloxmemoryapi-0.0.1.dist-info/WHEEL +5 -0
- robloxmemoryapi-0.0.1.dist-info/licenses/LICENSE.md +7 -0
- robloxmemoryapi-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
__all__ = ["RobloxRandom", "RobloxGameClient", "__version__"]
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RobloxRandom:
|
|
9
|
+
MULT = 6364136223846793005
|
|
10
|
+
INC = 105
|
|
11
|
+
MASK64 = (1 << 64) - 1
|
|
12
|
+
|
|
13
|
+
def __init__(self, seed):
|
|
14
|
+
s = math.floor(seed)
|
|
15
|
+
|
|
16
|
+
self._state = 0
|
|
17
|
+
self._inc = RobloxRandom.INC
|
|
18
|
+
self._next_internal() # warm-up #1
|
|
19
|
+
self._state = (self._state + s) & RobloxRandom.MASK64
|
|
20
|
+
self._next_internal() # warm-up #2
|
|
21
|
+
|
|
22
|
+
def _next_internal(self):
|
|
23
|
+
old = self._state
|
|
24
|
+
self._state = (old * RobloxRandom.MULT + self._inc) & RobloxRandom.MASK64
|
|
25
|
+
x = ((old >> 18) ^ old) >> 27
|
|
26
|
+
r = old >> 59
|
|
27
|
+
return ((x >> r) | (x << ((32 - r) & 31))) & 0xFFFFFFFF
|
|
28
|
+
|
|
29
|
+
def _next_fraction64(self):
|
|
30
|
+
lo = self._next_internal()
|
|
31
|
+
hi = self._next_internal()
|
|
32
|
+
bits = (hi << 32) | lo
|
|
33
|
+
return bits / 2**64
|
|
34
|
+
|
|
35
|
+
def NextNumber(self, minimum=0.0, maximum=1.0):
|
|
36
|
+
frac = self._next_fraction64()
|
|
37
|
+
return minimum + frac * (maximum - minimum)
|
|
38
|
+
|
|
39
|
+
def NextInteger(self, a, b=None):
|
|
40
|
+
if b is None:
|
|
41
|
+
u = a
|
|
42
|
+
r = self._next_internal()
|
|
43
|
+
return ((u * r) >> 32) + 1
|
|
44
|
+
else:
|
|
45
|
+
lo, hi = (a, b) if a <= b else (b, a)
|
|
46
|
+
u = hi - lo + 1
|
|
47
|
+
r = self._next_internal()
|
|
48
|
+
return ((u * r) >> 32) + lo
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RobloxGameClient:
|
|
52
|
+
def __init__(self, pid: int = None, process_name: str = "RobloxPlayerBeta.exe"):
|
|
53
|
+
if platform.system() != "Windows":
|
|
54
|
+
self.failed = True
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
from .utils.memory import (
|
|
58
|
+
EvasiveProcess,
|
|
59
|
+
PROCESS_QUERY_INFORMATION,
|
|
60
|
+
PROCESS_VM_READ,
|
|
61
|
+
get_pid_by_name,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if pid is None:
|
|
65
|
+
self.pid = get_pid_by_name(process_name)
|
|
66
|
+
else:
|
|
67
|
+
self.pid = pid
|
|
68
|
+
|
|
69
|
+
if self.pid is None or self.pid == 0:
|
|
70
|
+
raise ValueError("Failed to get PID.")
|
|
71
|
+
|
|
72
|
+
self.memory_module = EvasiveProcess(self.pid, PROCESS_VM_READ | PROCESS_QUERY_INFORMATION)
|
|
73
|
+
self.failed = False
|
|
74
|
+
|
|
75
|
+
def close(self):
|
|
76
|
+
self.memory_module.close()
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def DataModel(self):
|
|
80
|
+
if platform.system() != "Windows":
|
|
81
|
+
raise RuntimeError("This module is only compatible with Windows.")
|
|
82
|
+
elif self.failed:
|
|
83
|
+
raise RuntimeError("There was an error while getting access to memory. Please try again later.")
|
|
84
|
+
|
|
85
|
+
from .utils.rbx.instance import DataModel
|
|
86
|
+
return DataModel(self.memory_module)
|
|
87
|
+
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import struct
|
|
3
|
+
from ctypes import wintypes
|
|
4
|
+
|
|
5
|
+
ntdll = ctypes.WinDLL('ntdll.dll')
|
|
6
|
+
psapi = ctypes.WinDLL('psapi.dll')
|
|
7
|
+
kernel32 = ctypes.WinDLL('kernel32.dll')
|
|
8
|
+
|
|
9
|
+
kernel32.VirtualAlloc.restype = wintypes.LPVOID
|
|
10
|
+
kernel32.VirtualAlloc.argtypes = [wintypes.LPVOID, ctypes.c_size_t, wintypes.DWORD, wintypes.DWORD]
|
|
11
|
+
kernel32.GetProcAddress.restype = wintypes.LPVOID
|
|
12
|
+
kernel32.GetProcAddress.argtypes = [wintypes.HMODULE, wintypes.LPCSTR]
|
|
13
|
+
|
|
14
|
+
NTSTATUS = wintypes.LONG
|
|
15
|
+
HANDLE = wintypes.HANDLE
|
|
16
|
+
DWORD = wintypes.DWORD
|
|
17
|
+
LPVOID = wintypes.LPVOID
|
|
18
|
+
HMODULE = wintypes.HMODULE
|
|
19
|
+
BOOL = wintypes.BOOL
|
|
20
|
+
|
|
21
|
+
PROCESS_QUERY_INFORMATION = 0x0400
|
|
22
|
+
PROCESS_VM_READ = 0x0010
|
|
23
|
+
LIST_MODULES_ALL = 0x03
|
|
24
|
+
STATUS_SUCCESS = 0
|
|
25
|
+
MEM_COMMIT = 0x1000
|
|
26
|
+
MEM_RESERVE = 0x2000
|
|
27
|
+
PAGE_EXECUTE_READWRITE = 0x40
|
|
28
|
+
NTDLL_HANDLE = ntdll._handle
|
|
29
|
+
|
|
30
|
+
class CLIENT_ID(ctypes.Structure):
|
|
31
|
+
_fields_ = [
|
|
32
|
+
("UniqueProcess", HANDLE),
|
|
33
|
+
("UniqueThread", HANDLE),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
class OBJECT_ATTRIBUTES(ctypes.Structure):
|
|
37
|
+
_fields_ = [
|
|
38
|
+
("Length", wintypes.ULONG),
|
|
39
|
+
("RootDirectory", HANDLE),
|
|
40
|
+
("ObjectName", LPVOID),
|
|
41
|
+
("Attributes", wintypes.ULONG),
|
|
42
|
+
("SecurityDescriptor", LPVOID),
|
|
43
|
+
("SecurityQualityOfService", LPVOID),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
def get_syscall_number(function_name: str) -> int | None:
|
|
47
|
+
func_address = kernel32.GetProcAddress(NTDLL_HANDLE, function_name.encode('ascii'))
|
|
48
|
+
if not func_address:
|
|
49
|
+
return None
|
|
50
|
+
buffer = (ctypes.c_ubyte * 8).from_address(func_address)
|
|
51
|
+
if tuple(buffer[0:4]) == (0x4c, 0x8b, 0xd1, 0xb8):
|
|
52
|
+
return int.from_bytes(bytes(buffer[4:8]), 'little')
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def create_syscall_function(syscall_number, func_prototype):
|
|
56
|
+
assembly_stub = b'\x4C\x8B\xD1' + \
|
|
57
|
+
b'\xB8' + syscall_number.to_bytes(4, 'little') + \
|
|
58
|
+
b'\x0F\x05' + \
|
|
59
|
+
b'\xC3'
|
|
60
|
+
|
|
61
|
+
exec_mem = kernel32.VirtualAlloc(None, len(assembly_stub), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)
|
|
62
|
+
if not exec_mem:
|
|
63
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
64
|
+
ctypes.memmove(exec_mem, assembly_stub, len(assembly_stub))
|
|
65
|
+
return func_prototype(exec_mem)
|
|
66
|
+
|
|
67
|
+
NtOpenProcessProto = ctypes.WINFUNCTYPE(
|
|
68
|
+
NTSTATUS,
|
|
69
|
+
ctypes.POINTER(HANDLE),
|
|
70
|
+
DWORD,
|
|
71
|
+
ctypes.POINTER(OBJECT_ATTRIBUTES),
|
|
72
|
+
ctypes.POINTER(CLIENT_ID)
|
|
73
|
+
)
|
|
74
|
+
NtReadVirtualMemoryProto = ctypes.WINFUNCTYPE(
|
|
75
|
+
NTSTATUS,
|
|
76
|
+
HANDLE,
|
|
77
|
+
LPVOID,
|
|
78
|
+
LPVOID,
|
|
79
|
+
ctypes.c_ulong,
|
|
80
|
+
ctypes.POINTER(ctypes.c_ulong)
|
|
81
|
+
)
|
|
82
|
+
NtCloseProto = ctypes.WINFUNCTYPE(NTSTATUS, HANDLE)
|
|
83
|
+
|
|
84
|
+
syscall_id_open = get_syscall_number("NtOpenProcess")
|
|
85
|
+
syscall_id_read = get_syscall_number("NtReadVirtualMemory")
|
|
86
|
+
syscall_id_close = get_syscall_number("NtClose")
|
|
87
|
+
|
|
88
|
+
if not all([syscall_id_open, syscall_id_read, syscall_id_close]):
|
|
89
|
+
raise RuntimeError("Could not find required syscall numbers.")
|
|
90
|
+
|
|
91
|
+
nt_open_process_syscall = create_syscall_function(syscall_id_open, NtOpenProcessProto)
|
|
92
|
+
nt_read_virtual_memory_syscall = create_syscall_function(syscall_id_read, NtReadVirtualMemoryProto)
|
|
93
|
+
nt_close_syscall = create_syscall_function(syscall_id_close, NtCloseProto)
|
|
94
|
+
|
|
95
|
+
psapi.EnumProcessModulesEx.argtypes = [
|
|
96
|
+
HANDLE,
|
|
97
|
+
ctypes.POINTER(HMODULE),
|
|
98
|
+
DWORD,
|
|
99
|
+
ctypes.POINTER(DWORD),
|
|
100
|
+
DWORD
|
|
101
|
+
]
|
|
102
|
+
psapi.EnumProcessModulesEx.restype = BOOL
|
|
103
|
+
|
|
104
|
+
def _get_module_base(process_handle: HANDLE) -> int:
|
|
105
|
+
try:
|
|
106
|
+
modules_arr_size = 256
|
|
107
|
+
modules_arr = (HMODULE * modules_arr_size)()
|
|
108
|
+
needed = DWORD(0)
|
|
109
|
+
psapi.EnumProcessModulesEx(
|
|
110
|
+
process_handle,
|
|
111
|
+
modules_arr,
|
|
112
|
+
ctypes.sizeof(modules_arr),
|
|
113
|
+
ctypes.byref(needed),
|
|
114
|
+
LIST_MODULES_ALL
|
|
115
|
+
)
|
|
116
|
+
if needed.value > ctypes.sizeof(modules_arr):
|
|
117
|
+
new_size = needed.value // ctypes.sizeof(HMODULE)
|
|
118
|
+
modules_arr = (HMODULE * new_size)()
|
|
119
|
+
success = psapi.EnumProcessModulesEx(
|
|
120
|
+
process_handle,
|
|
121
|
+
modules_arr,
|
|
122
|
+
ctypes.sizeof(modules_arr),
|
|
123
|
+
ctypes.byref(needed),
|
|
124
|
+
LIST_MODULES_ALL
|
|
125
|
+
)
|
|
126
|
+
if not success:
|
|
127
|
+
return 0
|
|
128
|
+
if needed.value > 0:
|
|
129
|
+
return modules_arr[0] if modules_arr[0] else 0
|
|
130
|
+
return 0
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"An exception occurred in _get_module_base: {e}")
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
def get_pid_by_name(process_name: str) -> int:
|
|
136
|
+
class PROCESSENTRY32(ctypes.Structure):
|
|
137
|
+
_fields_ = [
|
|
138
|
+
("dwSize", DWORD),
|
|
139
|
+
("cntUsage", DWORD),
|
|
140
|
+
("th32ProcessID", DWORD),
|
|
141
|
+
("th32DefaultHeapID", ctypes.POINTER(wintypes.ULONG)),
|
|
142
|
+
("th32ModuleID", DWORD),
|
|
143
|
+
("cntThreads", DWORD),
|
|
144
|
+
("th32ParentProcessID", DWORD),
|
|
145
|
+
("pcPriClassBase", wintypes.LONG),
|
|
146
|
+
("dwFlags", DWORD),
|
|
147
|
+
("szExeFile", wintypes.CHAR * 260)
|
|
148
|
+
]
|
|
149
|
+
snapshot = kernel32.CreateToolhelp32Snapshot(2, 0)
|
|
150
|
+
if not snapshot or snapshot == wintypes.HANDLE(-1).value:
|
|
151
|
+
raise ctypes.WinError()
|
|
152
|
+
entry = PROCESSENTRY32()
|
|
153
|
+
entry.dwSize = ctypes.sizeof(PROCESSENTRY32)
|
|
154
|
+
try:
|
|
155
|
+
if not kernel32.Process32First(snapshot, ctypes.byref(entry)):
|
|
156
|
+
return 0
|
|
157
|
+
while True:
|
|
158
|
+
if entry.szExeFile.decode('utf-8', errors='ignore') == process_name:
|
|
159
|
+
return entry.th32ProcessID
|
|
160
|
+
if not kernel32.Process32Next(snapshot, ctypes.byref(entry)):
|
|
161
|
+
break
|
|
162
|
+
finally:
|
|
163
|
+
kernel32.CloseHandle(snapshot)
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
class EvasiveProcess:
|
|
167
|
+
def __init__(self, pid: int, access: DWORD):
|
|
168
|
+
self.pid = pid
|
|
169
|
+
self.access = access
|
|
170
|
+
self.handle = HANDLE(0)
|
|
171
|
+
self.base = 0
|
|
172
|
+
|
|
173
|
+
object_attributes = OBJECT_ATTRIBUTES()
|
|
174
|
+
client_id = CLIENT_ID()
|
|
175
|
+
client_id.UniqueProcess = HANDLE(pid)
|
|
176
|
+
object_attributes.Length = ctypes.sizeof(OBJECT_ATTRIBUTES)
|
|
177
|
+
|
|
178
|
+
status = nt_open_process_syscall(
|
|
179
|
+
ctypes.byref(self.handle),
|
|
180
|
+
access,
|
|
181
|
+
ctypes.byref(object_attributes),
|
|
182
|
+
ctypes.byref(client_id)
|
|
183
|
+
)
|
|
184
|
+
if status != STATUS_SUCCESS:
|
|
185
|
+
raise ctypes.WinError(f"NtOpenProcess failed with NTSTATUS: 0x{status:X}")
|
|
186
|
+
self.base = _get_module_base(self.handle)
|
|
187
|
+
if self.base == 0:
|
|
188
|
+
self.close()
|
|
189
|
+
raise ConnectionError("Failed to get module base address.")
|
|
190
|
+
|
|
191
|
+
def read(self, address: int, size: int) -> bytes:
|
|
192
|
+
if not self.handle or self.handle.value == 0:
|
|
193
|
+
raise ValueError("Process handle is not valid.")
|
|
194
|
+
buffer = ctypes.create_string_buffer(size)
|
|
195
|
+
bytes_read = ctypes.c_ulong(0)
|
|
196
|
+
status = nt_read_virtual_memory_syscall(
|
|
197
|
+
self.handle,
|
|
198
|
+
LPVOID(address),
|
|
199
|
+
buffer,
|
|
200
|
+
size,
|
|
201
|
+
ctypes.byref(bytes_read)
|
|
202
|
+
)
|
|
203
|
+
if status != STATUS_SUCCESS:
|
|
204
|
+
raise OSError(f"NtReadVirtualMemory failed with NTSTATUS: 0x{status:X}")
|
|
205
|
+
return buffer.raw[:bytes_read.value]
|
|
206
|
+
|
|
207
|
+
# numbers #
|
|
208
|
+
def read_int(self, address: int) -> int:
|
|
209
|
+
buffer = self.read(address, 4)
|
|
210
|
+
return int.from_bytes(buffer, 'little') if len(buffer) == 4 else 0
|
|
211
|
+
|
|
212
|
+
def read_long(self, address: int) -> int:
|
|
213
|
+
buffer = self.read(address, 8)
|
|
214
|
+
return int.from_bytes(buffer, 'little') if len(buffer) == 8 else 0
|
|
215
|
+
|
|
216
|
+
def read_double(self, address: int) -> float:
|
|
217
|
+
try:
|
|
218
|
+
double_bytes = self.read(address, 8)
|
|
219
|
+
return struct.unpack('<d', double_bytes)[0] if len(double_bytes) == 8 else 0.0
|
|
220
|
+
except (OSError, struct.error):
|
|
221
|
+
return 0.0
|
|
222
|
+
|
|
223
|
+
def read_float(self, address: int) -> float:
|
|
224
|
+
try:
|
|
225
|
+
float_bytes = self.read(address, 4)
|
|
226
|
+
return struct.unpack('f', float_bytes)[0] if len(float_bytes) == 4 else 0.0
|
|
227
|
+
except (OSError, struct.error):
|
|
228
|
+
return 0.0
|
|
229
|
+
|
|
230
|
+
def read_floats(self, address: int, amount: int):
|
|
231
|
+
try:
|
|
232
|
+
bulk_float_bytes = self.read(address, 4 * amount)
|
|
233
|
+
floats = []
|
|
234
|
+
for i in range(amount):
|
|
235
|
+
start_range = i * 4
|
|
236
|
+
float_bytes = bulk_float_bytes[start_range:start_range + 4]
|
|
237
|
+
|
|
238
|
+
if len(float_bytes) == 4:
|
|
239
|
+
floats.append(struct.unpack('f', float_bytes)[0])
|
|
240
|
+
else:
|
|
241
|
+
floats.append(0.0)
|
|
242
|
+
|
|
243
|
+
return floats
|
|
244
|
+
except (OSError, struct.error) as e:
|
|
245
|
+
return [0.0]
|
|
246
|
+
|
|
247
|
+
# bool #
|
|
248
|
+
def read_bool(self, address: int) -> bool:
|
|
249
|
+
try:
|
|
250
|
+
bool_byte = self.read(address, 1)
|
|
251
|
+
if not bool_byte: return False
|
|
252
|
+
return bool(int.from_bytes(bool_byte, 'little'))
|
|
253
|
+
except OSError:
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
# string #
|
|
257
|
+
def read_raw_string(self, address: int, max_length: int = 256) -> str:
|
|
258
|
+
buffer = self.read(address, max_length)
|
|
259
|
+
null_pos = buffer.find(b'\x00')
|
|
260
|
+
valid_bytes = buffer[:null_pos] if null_pos != -1 else buffer
|
|
261
|
+
return valid_bytes.decode('utf-8', errors='ignore')
|
|
262
|
+
|
|
263
|
+
def read_string(self, address: int) -> str:
|
|
264
|
+
string_length = self.read_int(address + 0x10)
|
|
265
|
+
if string_length <= 15:
|
|
266
|
+
return self.read_raw_string(address, string_length)
|
|
267
|
+
else:
|
|
268
|
+
string_data_pointer = self.read_long(address)
|
|
269
|
+
return self.read_raw_string(string_data_pointer, string_length) if string_data_pointer else ""
|
|
270
|
+
|
|
271
|
+
#########
|
|
272
|
+
def close(self):
|
|
273
|
+
if self.handle and self.handle.value != 0:
|
|
274
|
+
nt_close_syscall(self.handle)
|
|
275
|
+
self.handle = HANDLE(0)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
from importlib import resources
|
|
4
|
+
|
|
5
|
+
Offsets = {}
|
|
6
|
+
def ParseOffsets(*DataSources):
|
|
7
|
+
for _, Data in enumerate(DataSources, start=1):
|
|
8
|
+
for OffsetName in Data:
|
|
9
|
+
try:
|
|
10
|
+
FormattedOffsetName = OffsetName.replace(" ", "")
|
|
11
|
+
OffsetHexadecimalValue = Data[OffsetName]
|
|
12
|
+
|
|
13
|
+
Offsets[FormattedOffsetName] = int(OffsetHexadecimalValue, 16)
|
|
14
|
+
except (ValueError, TypeError):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
OffsetsRequest = requests.get("https://offsets.ntgetwritewatch.workers.dev/offsets.json")
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
LoadedOffsetsRequest = requests.get(
|
|
22
|
+
"https://raw.githubusercontent.com/notpoiu/RobloxMemoryAPI/refs/heads/main/src/robloxmemoryapi/data/offsets.json"
|
|
23
|
+
)
|
|
24
|
+
LoadedOffsetsRequest.raise_for_status()
|
|
25
|
+
|
|
26
|
+
LoadedOffsets = LoadedOffsetsRequest.json()
|
|
27
|
+
except Exception:
|
|
28
|
+
try:
|
|
29
|
+
with resources.files("robloxmemoryapi.data").joinpath("offsets.json").open("r", encoding="utf-8") as f:
|
|
30
|
+
LoadedOffsets = json.load(f)
|
|
31
|
+
except Exception:
|
|
32
|
+
# Fallback defaults
|
|
33
|
+
LoadedOffsets = {
|
|
34
|
+
"Text": "0xC10",
|
|
35
|
+
"Character": "0x328",
|
|
36
|
+
"PrimaryPart": "0x268",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
ParseOffsets(LoadedOffsets, OffsetsRequest.json())
|
|
40
|
+
|
|
41
|
+
# CFrame Offsets
|
|
42
|
+
RotationMatriciesLengthBytes = 3 * 3 * 4
|
|
43
|
+
|
|
44
|
+
Offsets["CameraCFrame"] = Offsets["CameraPos"] - RotationMatriciesLengthBytes
|
|
45
|
+
Offsets["CFrame"] = Offsets["Position"] - RotationMatriciesLengthBytes
|
|
46
|
+
|
|
File without changes
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
def get_flat_matrix_column(matrix, column, invert_values=False):
|
|
4
|
+
stride = 3
|
|
5
|
+
|
|
6
|
+
if invert_values:
|
|
7
|
+
return [matrix[column + i * stride] * -1 for i in range(3)]
|
|
8
|
+
|
|
9
|
+
return [matrix[column + i * stride] for i in range(3)]
|
|
10
|
+
|
|
11
|
+
class UDim:
|
|
12
|
+
def __init__(self, scale=0, offset=0):
|
|
13
|
+
self.Scale = float(scale)
|
|
14
|
+
self.Offset = int(offset)
|
|
15
|
+
|
|
16
|
+
def __repr__(self):
|
|
17
|
+
return f"{{{self.Scale}, {self.Offset}}}"
|
|
18
|
+
|
|
19
|
+
class UDim2:
|
|
20
|
+
def __init__(self, scaleX=0, offsetX=0, scaleY=0, offsetY=0):
|
|
21
|
+
self.X = UDim(scaleX, offsetX)
|
|
22
|
+
self.Y = UDim(scaleY, offsetY)
|
|
23
|
+
|
|
24
|
+
def __repr__(self):
|
|
25
|
+
return f"{{{self.X}, {self.Y}}}"
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def fromScale(cls, scaleX, scaleY):
|
|
29
|
+
return cls(scaleX, 0, scaleY, 0)
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def fromOffset(cls, offsetX, offsetY):
|
|
33
|
+
return cls(0, offsetX, 0, offsetY)
|
|
34
|
+
|
|
35
|
+
class Vector2:
|
|
36
|
+
def __init__(self, x=0, y=0):
|
|
37
|
+
self.X = float(x)
|
|
38
|
+
self.Y = float(y)
|
|
39
|
+
|
|
40
|
+
def __repr__(self):
|
|
41
|
+
return f"{self.X}, {self.Y}"
|
|
42
|
+
|
|
43
|
+
def __eq__(self, other):
|
|
44
|
+
return (
|
|
45
|
+
isinstance(other, Vector2)
|
|
46
|
+
and self.X == other.X
|
|
47
|
+
and self.Y == other.Y
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
class Vector3:
|
|
51
|
+
def __init__(self, x=0, y=0, z=0):
|
|
52
|
+
self.X = float(x)
|
|
53
|
+
self.Y = float(y)
|
|
54
|
+
self.Z = float(z)
|
|
55
|
+
|
|
56
|
+
def __repr__(self):
|
|
57
|
+
return f"{self.X}, {self.Y}, {self.Z}"
|
|
58
|
+
|
|
59
|
+
def __eq__(self, other):
|
|
60
|
+
eps = 1e-9
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
isinstance(other, Vector3)
|
|
64
|
+
and abs(self.X - other.X) < eps
|
|
65
|
+
and abs(self.Y - other.Y) < eps
|
|
66
|
+
and abs(self.Z - other.Z) < eps
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# --- Arithmetic ---
|
|
70
|
+
def __add__(self, other):
|
|
71
|
+
if isinstance(other, Vector3):
|
|
72
|
+
return Vector3(self.X + other.X, self.Y + other.Y, self.Z + other.Z)
|
|
73
|
+
raise TypeError("Vector3 can only be added to Vector3")
|
|
74
|
+
|
|
75
|
+
def __sub__(self, other):
|
|
76
|
+
if isinstance(other, Vector3):
|
|
77
|
+
return Vector3(self.X - other.X, self.Y - other.Y, self.Z - other.Z)
|
|
78
|
+
raise TypeError("Vector3 can only be subtracted by Vector3")
|
|
79
|
+
|
|
80
|
+
def __mul__(self, other):
|
|
81
|
+
if isinstance(other, (int, float)):
|
|
82
|
+
return Vector3(self.X * other, self.Y * other, self.Z * other)
|
|
83
|
+
if isinstance(other, Vector3):
|
|
84
|
+
return Vector3(self.X * other.X, self.Y * other.Y, self.Z * other.Z)
|
|
85
|
+
raise TypeError("Vector3 can only be multiplied by Vector3 or a number")
|
|
86
|
+
|
|
87
|
+
__rmul__ = __mul__
|
|
88
|
+
|
|
89
|
+
def __truediv__(self, other):
|
|
90
|
+
if isinstance(other, (int, float)):
|
|
91
|
+
return Vector3(self.X / other, self.Y / other, self.Z / other)
|
|
92
|
+
if isinstance(other, Vector3):
|
|
93
|
+
return Vector3(self.X / other.X, self.Y / other.Y, self.Z / other.Z)
|
|
94
|
+
raise TypeError("Vector3 can only be divided by Vector3 or a number")
|
|
95
|
+
|
|
96
|
+
def __neg__(self):
|
|
97
|
+
return Vector3(-self.X, -self.Y, -self.Z)
|
|
98
|
+
|
|
99
|
+
# --- Vector math ---
|
|
100
|
+
def Dot(self, other):
|
|
101
|
+
return self.X * other.X + self.Y * other.Y + self.Z * other.Z
|
|
102
|
+
|
|
103
|
+
def Cross(self, other):
|
|
104
|
+
return Vector3(
|
|
105
|
+
self.Y * other.Z - self.Z * other.Y,
|
|
106
|
+
self.Z * other.X - self.X * other.Z,
|
|
107
|
+
self.X * other.Y - self.Y * other.X,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def Magnitude(self):
|
|
111
|
+
return math.sqrt(self.X**2 + self.Y**2 + self.Z**2)
|
|
112
|
+
|
|
113
|
+
def Unit(self):
|
|
114
|
+
m = self.Magnitude()
|
|
115
|
+
return Vector3(self.X / m, self.Y / m, self.Z / m) if m != 0 else Vector3()
|
|
116
|
+
|
|
117
|
+
def Lerp(self, other, alpha: float):
|
|
118
|
+
return self + (other - self) * alpha
|
|
119
|
+
|
|
120
|
+
class CFrame:
|
|
121
|
+
def __init__(self, position=None, right=None, up=None, look=None):
|
|
122
|
+
self.Position = position or Vector3(0, 0, 0)
|
|
123
|
+
|
|
124
|
+
if right is None and up is None and look is None:
|
|
125
|
+
self.RightVector = Vector3(1, 0, 0)
|
|
126
|
+
self.UpVector = Vector3(0, 1, 0)
|
|
127
|
+
self.LookVector = Vector3(0, 0, -1)
|
|
128
|
+
else:
|
|
129
|
+
r, u, l = self._orthonormal_basis(right, up, look)
|
|
130
|
+
self.RightVector, self.UpVector, self.LookVector = r, u, l
|
|
131
|
+
|
|
132
|
+
def __repr__(self):
|
|
133
|
+
return (
|
|
134
|
+
f"{self.Position}, {self.RightVector}, {self.UpVector}, {self.LookVector}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def new(cls, x=0, y=0, z=0):
|
|
139
|
+
return cls(position=Vector3(x, y, z))
|
|
140
|
+
|
|
141
|
+
# --- Rotation helpers ---
|
|
142
|
+
def _rotate_vector(self, v: Vector3) -> Vector3:
|
|
143
|
+
return (
|
|
144
|
+
self.RightVector * v.X +
|
|
145
|
+
self.UpVector * v.Y +
|
|
146
|
+
self.LookVector * v.Z
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def _inverse_rotate_vector(self, v: Vector3) -> Vector3:
|
|
150
|
+
# Multiply by transpose of rotation matrix
|
|
151
|
+
return Vector3(
|
|
152
|
+
v.Dot(self.RightVector),
|
|
153
|
+
v.Dot(self.UpVector),
|
|
154
|
+
v.Dot(self.LookVector)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _orthonormal_basis(right=None, up=None, look=None):
|
|
159
|
+
def unit(v: Vector3 | None):
|
|
160
|
+
return v.Unit() if isinstance(v, Vector3) else None
|
|
161
|
+
|
|
162
|
+
def nearly_parallel(a: Vector3, b: Vector3) -> bool:
|
|
163
|
+
ma, mb = a.Magnitude(), b.Magnitude()
|
|
164
|
+
if ma == 0 or mb == 0:
|
|
165
|
+
return True
|
|
166
|
+
return abs(a.Dot(b) / (ma * mb)) > 0.9999
|
|
167
|
+
|
|
168
|
+
def orthogonal_to(v: Vector3) -> Vector3:
|
|
169
|
+
fallback = Vector3(0, 1, 0)
|
|
170
|
+
if nearly_parallel(v, fallback):
|
|
171
|
+
fallback = Vector3(1, 0, 0)
|
|
172
|
+
return (fallback - v * fallback.Dot(v)).Unit()
|
|
173
|
+
|
|
174
|
+
r = unit(right)
|
|
175
|
+
u = unit(up)
|
|
176
|
+
l = unit(look)
|
|
177
|
+
|
|
178
|
+
if r is not None and u is not None:
|
|
179
|
+
u = (u - r * u.Dot(r)).Unit()
|
|
180
|
+
l = -r.Cross(u).Unit()
|
|
181
|
+
return r, u, l
|
|
182
|
+
|
|
183
|
+
if r is not None and l is not None:
|
|
184
|
+
l = (l - r * l.Dot(r)).Unit()
|
|
185
|
+
u = l.Cross(r).Unit()
|
|
186
|
+
return r, u, l
|
|
187
|
+
|
|
188
|
+
if u is not None and l is not None:
|
|
189
|
+
l = (l - u * l.Dot(l)).Unit()
|
|
190
|
+
r = l.Cross(u).Unit()
|
|
191
|
+
return r, u, l
|
|
192
|
+
|
|
193
|
+
if r is not None:
|
|
194
|
+
u = orthogonal_to(r)
|
|
195
|
+
l = -r.Cross(u).Unit()
|
|
196
|
+
return r, u, l
|
|
197
|
+
|
|
198
|
+
if u is not None:
|
|
199
|
+
r = orthogonal_to(u)
|
|
200
|
+
l = -r.Cross(u).Unit()
|
|
201
|
+
return r, u, l
|
|
202
|
+
|
|
203
|
+
if l is not None:
|
|
204
|
+
u = orthogonal_to(l)
|
|
205
|
+
r = l.Cross(u).Unit()
|
|
206
|
+
return r, u, l
|
|
207
|
+
|
|
208
|
+
return Vector3(1, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, -1)
|
|
209
|
+
|
|
210
|
+
# --- Operators ---
|
|
211
|
+
def __mul__(self, other):
|
|
212
|
+
if isinstance(other, CFrame):
|
|
213
|
+
rotated_pos = self._rotate_vector(other.Position)
|
|
214
|
+
new_pos = self.Position + rotated_pos
|
|
215
|
+
|
|
216
|
+
r = self._rotate_vector(other.RightVector)
|
|
217
|
+
u = self._rotate_vector(other.UpVector)
|
|
218
|
+
l = self._rotate_vector(other.LookVector)
|
|
219
|
+
|
|
220
|
+
return CFrame(new_pos, r, u, l)
|
|
221
|
+
|
|
222
|
+
if isinstance(other, Vector3):
|
|
223
|
+
return self.Position + self._rotate_vector(other)
|
|
224
|
+
|
|
225
|
+
raise TypeError("CFrame can only be multiplied by CFrame or Vector3")
|
|
226
|
+
|
|
227
|
+
def __add__(self, other):
|
|
228
|
+
if isinstance(other, Vector3):
|
|
229
|
+
return CFrame(self.Position + other,
|
|
230
|
+
self.RightVector, self.UpVector, self.LookVector)
|
|
231
|
+
raise TypeError("CFrame can only be added to Vector3")
|
|
232
|
+
|
|
233
|
+
def __sub__(self, other):
|
|
234
|
+
if isinstance(other, Vector3):
|
|
235
|
+
return CFrame(self.Position - other,
|
|
236
|
+
self.RightVector, self.UpVector, self.LookVector)
|
|
237
|
+
raise TypeError("CFrame can only be subtracted by Vector3")
|
|
238
|
+
|
|
239
|
+
# --- API Methods ---
|
|
240
|
+
def Inverse(self):
|
|
241
|
+
# Rotation inverse is transpose
|
|
242
|
+
r = Vector3(self.RightVector.X, self.UpVector.X, self.LookVector.X)
|
|
243
|
+
u = Vector3(self.RightVector.Y, self.UpVector.Y, self.LookVector.Y)
|
|
244
|
+
l = Vector3(self.RightVector.Z, self.UpVector.Z, self.LookVector.Z)
|
|
245
|
+
|
|
246
|
+
inv_pos = Vector3(
|
|
247
|
+
-self.Position.Dot(r),
|
|
248
|
+
-self.Position.Dot(u),
|
|
249
|
+
-self.Position.Dot(l),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return CFrame(inv_pos, r, u, l)
|
|
253
|
+
|
|
254
|
+
def ToWorldSpace(self, cf):
|
|
255
|
+
return self * cf
|
|
256
|
+
|
|
257
|
+
def ToObjectSpace(self, cf):
|
|
258
|
+
return self.Inverse() * cf
|
|
259
|
+
|
|
260
|
+
def GetComponents(self):
|
|
261
|
+
return tuple([
|
|
262
|
+
self.RightVector.X, self.UpVector.X, self.LookVector.X,
|
|
263
|
+
self.RightVector.Y, self.UpVector.Y, self.LookVector.Y,
|
|
264
|
+
self.RightVector.Z, self.UpVector.Z, self.LookVector.Z,
|
|
265
|
+
self.Position.X, self.Position.Y, self.Position.Z
|
|
266
|
+
])
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
from ..offsets import *
|
|
2
|
+
import time, math
|
|
3
|
+
from .datastructures import *
|
|
4
|
+
|
|
5
|
+
# Normal Classes #
|
|
6
|
+
class RBXInstance:
|
|
7
|
+
def __init__(self, address, memory_module):
|
|
8
|
+
self.raw_address = address
|
|
9
|
+
self.memory_module = memory_module
|
|
10
|
+
|
|
11
|
+
def __eq__(self, value):
|
|
12
|
+
return value.raw_address == self.raw_address
|
|
13
|
+
|
|
14
|
+
def __getattr__(self, key):
|
|
15
|
+
return self.FindFirstChild(key)
|
|
16
|
+
|
|
17
|
+
# utilities #
|
|
18
|
+
@property
|
|
19
|
+
def primitive_address(self):
|
|
20
|
+
part_primitive_pointer = self.raw_address + Offsets["Primitive"]
|
|
21
|
+
part_primitive = int.from_bytes(self.memory_module.read(part_primitive_pointer, 8), 'little')
|
|
22
|
+
return part_primitive
|
|
23
|
+
|
|
24
|
+
# props #
|
|
25
|
+
@property
|
|
26
|
+
def Parent(self):
|
|
27
|
+
parent_pointer = int.from_bytes(self.memory_module.read(self.raw_address + Offsets["Parent"], 8), 'little')
|
|
28
|
+
if parent_pointer == 0:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
return RBXInstance(parent_pointer, self.memory_module)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def Name(self):
|
|
35
|
+
name_address_pointer = self.raw_address + Offsets["Name"]
|
|
36
|
+
name_address = int.from_bytes(self.memory_module.read(name_address_pointer, 8), 'little')
|
|
37
|
+
return self.memory_module.read_string(name_address)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def ClassName(self):
|
|
41
|
+
class_descriptor_address = int.from_bytes(
|
|
42
|
+
self.memory_module.read(self.raw_address + Offsets["ClassDescriptor"], 8),
|
|
43
|
+
'little'
|
|
44
|
+
)
|
|
45
|
+
class_name_address = int.from_bytes(
|
|
46
|
+
self.memory_module.read(class_descriptor_address + Offsets["ClassDescriptorToClassName"], 8),
|
|
47
|
+
'little'
|
|
48
|
+
)
|
|
49
|
+
return self.memory_module.read_string(class_name_address)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def CFrame(self):
|
|
53
|
+
className = self.ClassName
|
|
54
|
+
|
|
55
|
+
CFrameDataMatriciesLength = 12 # 3x4 matrix
|
|
56
|
+
|
|
57
|
+
if "part" in className.lower():
|
|
58
|
+
CFrameData = self.memory_module.read_floats(self.primitive_address + Offsets["CFrame"], CFrameDataMatriciesLength)
|
|
59
|
+
elif className == "Camera":
|
|
60
|
+
CFrameData = self.memory_module.read_floats(self.raw_address + Offsets["CameraCFrame"], CFrameDataMatriciesLength)
|
|
61
|
+
else:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
RightVectorData = get_flat_matrix_column(CFrameData, 0)
|
|
65
|
+
UpVectorData = get_flat_matrix_column(CFrameData, 1)
|
|
66
|
+
LookVectorData = get_flat_matrix_column(CFrameData, 2, invert_values=True)
|
|
67
|
+
PositionData = CFrameData[9:12]
|
|
68
|
+
|
|
69
|
+
return CFrame(
|
|
70
|
+
Vector3(*PositionData),
|
|
71
|
+
Vector3(*RightVectorData),
|
|
72
|
+
Vector3(*UpVectorData),
|
|
73
|
+
Vector3(*LookVectorData)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def Position(self):
|
|
78
|
+
className = self.ClassName
|
|
79
|
+
if "part" in className.lower():
|
|
80
|
+
position_vector3 = self.memory_module.read_floats(self.primitive_address + Offsets["Position"], 3)
|
|
81
|
+
return Vector3(*position_vector3)
|
|
82
|
+
elif className == "Camera":
|
|
83
|
+
position_vector3 = self.memory_module.read_floats(self.raw_address + Offsets["CameraPos"], 3)
|
|
84
|
+
return Vector3(*position_vector3)
|
|
85
|
+
else:
|
|
86
|
+
try:
|
|
87
|
+
x = self.memory_module.read_float(self.raw_address + Offsets["FramePositionX"])
|
|
88
|
+
x_offset = self.memory_module.read_int(self.raw_address + Offsets["FramePositionOffsetX"])
|
|
89
|
+
|
|
90
|
+
y = self.memory_module.read_float(self.raw_address + Offsets["FramePositionY"])
|
|
91
|
+
y_offset = self.memory_module.read_int(self.raw_address + Offsets["FramePositionOffsetY"])
|
|
92
|
+
|
|
93
|
+
return UDim2(x, x_offset, y, y_offset)
|
|
94
|
+
except (KeyError, OSError) as e:
|
|
95
|
+
print(f"Error reading position: {e}")
|
|
96
|
+
return (0.0, 0, 0.0, 0)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def Size(self):
|
|
100
|
+
if "part" in self.ClassName.lower():
|
|
101
|
+
size_vector3 = self.memory_module.read_floats(self.primitive_address + Offsets["PartSize"], 3)
|
|
102
|
+
return Vector3(*size_vector3)
|
|
103
|
+
else:
|
|
104
|
+
try:
|
|
105
|
+
x = self.memory_module.read_float(self.raw_address + Offsets["FrameSizeX"])
|
|
106
|
+
y = self.memory_module.read_float(self.raw_address + Offsets["FrameSizeY"])
|
|
107
|
+
return (x, y)
|
|
108
|
+
except (KeyError, OSError) as e:
|
|
109
|
+
print(f"Error reading position: {e}")
|
|
110
|
+
return (0.0, 0.0)
|
|
111
|
+
|
|
112
|
+
# XXXXValue props #
|
|
113
|
+
@property
|
|
114
|
+
def Value(self):
|
|
115
|
+
classname = self.ClassName
|
|
116
|
+
if classname == "StringValue":
|
|
117
|
+
return self.memory_module.read_string(self.raw_address + Offsets["Value"])
|
|
118
|
+
|
|
119
|
+
elif classname == "IntValue":
|
|
120
|
+
return self.memory_module.read_int(self.raw_address + Offsets["Value"])
|
|
121
|
+
|
|
122
|
+
elif classname == "NumberValue":
|
|
123
|
+
return self.memory_module.read_double(self.raw_address + Offsets["Value"])
|
|
124
|
+
|
|
125
|
+
elif classname == "BoolValue":
|
|
126
|
+
return self.memory_module.read_bool(self.raw_address + Offsets["Value"])
|
|
127
|
+
|
|
128
|
+
elif classname == "ObjectValue":
|
|
129
|
+
object_pointer = self.raw_address + Offsets["Value"]
|
|
130
|
+
object_address = int.from_bytes(self.memory_module.read(object_pointer, 8), 'little')
|
|
131
|
+
|
|
132
|
+
return RBXInstance(object_address, self.memory_module)
|
|
133
|
+
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
# text props #
|
|
137
|
+
@property
|
|
138
|
+
def Text(self):
|
|
139
|
+
if "text" in self.ClassName.lower():
|
|
140
|
+
return self.memory_module.read_string(self.raw_address + Offsets["Text"])
|
|
141
|
+
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
# humanoid props #
|
|
145
|
+
@property
|
|
146
|
+
def WalkSpeed(self):
|
|
147
|
+
if self.ClassName != "Humanoid":
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
return self.memory_module.read_float(self.raw_address + Offsets["WalkSpeed"])
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def JumpPower(self):
|
|
154
|
+
if self.ClassName != "Humanoid":
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
return self.memory_module.read_float(self.raw_address + Offsets["JumpPower"])
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def Health(self):
|
|
161
|
+
if self.ClassName != "Humanoid":
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
return self.memory_module.read_float(self.raw_address + Offsets["Health"])
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def MaxHealth(self):
|
|
168
|
+
if self.ClassName != "Humanoid":
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
return self.memory_module.read_float(self.raw_address + Offsets["MaxHealth"])
|
|
172
|
+
|
|
173
|
+
# model props #
|
|
174
|
+
@property
|
|
175
|
+
def PrimaryPart(self):
|
|
176
|
+
if self.ClassName != "Model":
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
parent_pointer = int.from_bytes(self.memory_module.read(self.raw_address + Offsets["PrimaryPart"], 8), 'little')
|
|
180
|
+
if parent_pointer == 0:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
return RBXInstance(parent_pointer, self.memory_module)
|
|
184
|
+
|
|
185
|
+
# functions #
|
|
186
|
+
def GetChildren(self):
|
|
187
|
+
children = []
|
|
188
|
+
children_pointer = int.from_bytes(self.memory_module.read(self.raw_address + Offsets["Children"], 8), 'little')
|
|
189
|
+
|
|
190
|
+
if children_pointer == 0:
|
|
191
|
+
return children
|
|
192
|
+
|
|
193
|
+
children_start = int.from_bytes(self.memory_module.read(children_pointer, 8), 'little')
|
|
194
|
+
children_end = int.from_bytes(self.memory_module.read(children_pointer + Offsets["ChildrenEnd"], 8), 'little')
|
|
195
|
+
|
|
196
|
+
for child_address in range(children_start, children_end, 0x10):
|
|
197
|
+
child_pointer_bytes = self.memory_module.read(child_address, 8)
|
|
198
|
+
child_pointer = int.from_bytes(child_pointer_bytes, 'little')
|
|
199
|
+
|
|
200
|
+
if child_pointer != 0:
|
|
201
|
+
children.append(RBXInstance(child_pointer, self.memory_module))
|
|
202
|
+
|
|
203
|
+
return children
|
|
204
|
+
|
|
205
|
+
def GetFullName(self):
|
|
206
|
+
if self.ClassName == "DataModel":
|
|
207
|
+
return self.Name
|
|
208
|
+
|
|
209
|
+
ObjectPointer = self
|
|
210
|
+
ObjectPath = self.Name
|
|
211
|
+
|
|
212
|
+
while True:
|
|
213
|
+
if ObjectPointer.Parent.ClassName == "DataModel":
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
ObjectPointer = ObjectPointer.Parent
|
|
217
|
+
ObjectPath = f"{ObjectPointer.Name}." + ObjectPath
|
|
218
|
+
|
|
219
|
+
return ObjectPath
|
|
220
|
+
|
|
221
|
+
def GetDescendants(self):
|
|
222
|
+
descendants = []
|
|
223
|
+
for child in self.GetChildren():
|
|
224
|
+
descendants.append(child)
|
|
225
|
+
descendants.extend(child.GetDescendants())
|
|
226
|
+
return descendants
|
|
227
|
+
|
|
228
|
+
def FindFirstChildOfClass(self, classname):
|
|
229
|
+
for child in self.GetChildren():
|
|
230
|
+
if child.ClassName == classname:
|
|
231
|
+
return child
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
def FindFirstChild(self, name, recursive=False):
|
|
235
|
+
try:
|
|
236
|
+
children = self.GetChildren()
|
|
237
|
+
for child in children:
|
|
238
|
+
if child.Name == name:
|
|
239
|
+
return child
|
|
240
|
+
|
|
241
|
+
if recursive:
|
|
242
|
+
for child in children:
|
|
243
|
+
found_descendant = child.FindFirstChild(name, recursive=True)
|
|
244
|
+
if found_descendant:
|
|
245
|
+
return found_descendant
|
|
246
|
+
except: pass
|
|
247
|
+
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
def WaitForChild(self, name, memoryhandler, timeout=5):
|
|
251
|
+
start = time.time()
|
|
252
|
+
child = None
|
|
253
|
+
|
|
254
|
+
while time.time() - start < timeout:
|
|
255
|
+
child = self.FindFirstChild(name)
|
|
256
|
+
if child is not None: break
|
|
257
|
+
if not (memoryhandler.game and not memoryhandler.game.failed): break
|
|
258
|
+
time.sleep(0.1)
|
|
259
|
+
|
|
260
|
+
return child
|
|
261
|
+
|
|
262
|
+
class PlayerClass(RBXInstance):
|
|
263
|
+
def __init__(self, memory_module, player: RBXInstance):
|
|
264
|
+
super().__init__(player.raw_address, memory_module)
|
|
265
|
+
self.memory_module = memory_module
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
if player.ClassName != "Player":
|
|
269
|
+
self.failed = True
|
|
270
|
+
else:
|
|
271
|
+
self.instance = player
|
|
272
|
+
except (KeyError, OSError):
|
|
273
|
+
self.failed = True
|
|
274
|
+
|
|
275
|
+
# props #
|
|
276
|
+
@property
|
|
277
|
+
def Character(self) -> RBXInstance | None:
|
|
278
|
+
addr = int.from_bytes(self.memory_module.read(self.instance.raw_address + Offsets["Character"], 8), 'little')
|
|
279
|
+
if addr == 0:
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
return RBXInstance(addr, self.memory_module)
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def DisplayName(self):
|
|
286
|
+
return self.memory_module.read_string(self.raw_address + Offsets["DisplayName"])
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def UserId(self):
|
|
290
|
+
return self.memory_module.read_long(self.raw_address + Offsets["UserId"])
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def Team(self):
|
|
294
|
+
addr = int.from_bytes(self.memory_module.read(self.instance.raw_address + Offsets["Team"], 8), 'little')
|
|
295
|
+
if addr == 0:
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
return RBXInstance(addr, self.memory_module)
|
|
299
|
+
|
|
300
|
+
class CameraClass(RBXInstance):
|
|
301
|
+
def __init__(self, memory_module, camera: RBXInstance):
|
|
302
|
+
super().__init__(camera.raw_address, memory_module)
|
|
303
|
+
self.memory_module = memory_module
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
if camera.ClassName != "Camera":
|
|
307
|
+
self.failed = True
|
|
308
|
+
else:
|
|
309
|
+
self.instance = camera
|
|
310
|
+
except (KeyError, OSError):
|
|
311
|
+
self.failed = True
|
|
312
|
+
|
|
313
|
+
# props #
|
|
314
|
+
@property
|
|
315
|
+
def FieldOfView(self):
|
|
316
|
+
return self.FieldOfViewRadians * (180/math.pi)
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def FieldOfViewRadians(self):
|
|
320
|
+
return self.memory_module.read_float(self.raw_address + Offsets["FOV"])
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def ViewportSize(self):
|
|
324
|
+
SizeData = self.memory_module.read_floats(self.raw_address + Offsets["ViewportSize"], 2)
|
|
325
|
+
return Vector2(*SizeData)
|
|
326
|
+
|
|
327
|
+
# Service #
|
|
328
|
+
class ServiceBase:
|
|
329
|
+
def __init__(self):
|
|
330
|
+
self.instance = None
|
|
331
|
+
self.failed = False
|
|
332
|
+
|
|
333
|
+
# expose instance functions #
|
|
334
|
+
def __getattr__(self, name):
|
|
335
|
+
# instance #
|
|
336
|
+
if self.instance is not None:
|
|
337
|
+
return getattr(self.instance, name)
|
|
338
|
+
|
|
339
|
+
return self.instance.FindFirstChild(name)
|
|
340
|
+
|
|
341
|
+
class DataModel(ServiceBase):
|
|
342
|
+
def __init__(self, memory_module):
|
|
343
|
+
super().__init__()
|
|
344
|
+
self.memory_module = memory_module
|
|
345
|
+
|
|
346
|
+
self.error = None
|
|
347
|
+
try:
|
|
348
|
+
if Offsets.get("DataModelPointer") is not None:
|
|
349
|
+
datamodel_addr = Offsets["DataModelPointer"]
|
|
350
|
+
else:
|
|
351
|
+
fake_dm_pointer_offset = Offsets["FakeDataModelPointer"]
|
|
352
|
+
fake_dm_pointer_addr = memory_module.base + fake_dm_pointer_offset
|
|
353
|
+
fake_dm_pointer_val = int.from_bytes(memory_module.read(fake_dm_pointer_addr, 8), 'little')
|
|
354
|
+
|
|
355
|
+
dm_to_datamodel_offset = Offsets["FakeDataModelToDataModel"]
|
|
356
|
+
datamodel_addr_ptr = fake_dm_pointer_val + dm_to_datamodel_offset
|
|
357
|
+
datamodel_addr = int.from_bytes(memory_module.read(datamodel_addr_ptr, 8), 'little')
|
|
358
|
+
|
|
359
|
+
datamodel_instance = RBXInstance(datamodel_addr, memory_module)
|
|
360
|
+
|
|
361
|
+
if datamodel_instance.Name != "Ugc":
|
|
362
|
+
self.failed = True
|
|
363
|
+
else:
|
|
364
|
+
self.instance = datamodel_instance
|
|
365
|
+
except (KeyError, OSError) as e:
|
|
366
|
+
self.error = e
|
|
367
|
+
self.failed = True
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def PlaceId(self):
|
|
371
|
+
return self.memory_module.read_long(self.raw_address + Offsets["PlaceId"])
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def GameId(self):
|
|
375
|
+
return self.memory_module.read_long(self.raw_address + Offsets["GameId"])
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def JobId(self):
|
|
379
|
+
return self.memory_module.read_string(self.raw_address + Offsets["JobId"])
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def Players(self):
|
|
383
|
+
return PlayersService(self.memory_module, self)
|
|
384
|
+
|
|
385
|
+
@property
|
|
386
|
+
def Workspace(self):
|
|
387
|
+
return WorkspaceService(self.memory_module, self)
|
|
388
|
+
|
|
389
|
+
# class functions #
|
|
390
|
+
def GetService(self, name):
|
|
391
|
+
if self.failed: return
|
|
392
|
+
|
|
393
|
+
for instance in self.instance.GetChildren():
|
|
394
|
+
if instance.ClassName == name:
|
|
395
|
+
return instance
|
|
396
|
+
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
# Stuff
|
|
400
|
+
def IsLoaded(self):
|
|
401
|
+
return self.memory_module.read_bool(self.raw_address + Offsets["GameLoaded"])
|
|
402
|
+
|
|
403
|
+
class PlayersService(ServiceBase):
|
|
404
|
+
def __init__(self, memory_module, game: DataModel):
|
|
405
|
+
super().__init__()
|
|
406
|
+
self.memory_module = memory_module
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
players_instance: RBXInstance = game.GetService("Players")
|
|
410
|
+
if players_instance.ClassName != "Players":
|
|
411
|
+
self.failed = True
|
|
412
|
+
else:
|
|
413
|
+
self.instance = players_instance
|
|
414
|
+
except (KeyError, OSError):
|
|
415
|
+
self.failed = True
|
|
416
|
+
|
|
417
|
+
# props #
|
|
418
|
+
@property
|
|
419
|
+
def LocalPlayer(self) -> RBXInstance | None:
|
|
420
|
+
if self.failed: return
|
|
421
|
+
|
|
422
|
+
addr = int.from_bytes(self.memory_module.read(self.instance.raw_address + Offsets["LocalPlayer"], 8), 'little')
|
|
423
|
+
return PlayerClass(self.memory_module, RBXInstance(addr, self.memory_module))
|
|
424
|
+
|
|
425
|
+
def GetPlayers(self):
|
|
426
|
+
players = []
|
|
427
|
+
|
|
428
|
+
for instance in self.instance.GetChildren():
|
|
429
|
+
if instance.ClassName == "Player":
|
|
430
|
+
players.append(PlayerClass(self.memory_module, instance))
|
|
431
|
+
|
|
432
|
+
return players
|
|
433
|
+
|
|
434
|
+
class WorkspaceService(ServiceBase):
|
|
435
|
+
def __init__(self, memory_module, game: DataModel):
|
|
436
|
+
super().__init__()
|
|
437
|
+
self.memory_module = memory_module
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
workspace_instance: RBXInstance = game.GetService("Workspace")
|
|
441
|
+
if workspace_instance.ClassName != "Workspace":
|
|
442
|
+
self.failed = True
|
|
443
|
+
else:
|
|
444
|
+
self.instance = workspace_instance
|
|
445
|
+
except (KeyError, OSError):
|
|
446
|
+
self.failed = True
|
|
447
|
+
|
|
448
|
+
# props #
|
|
449
|
+
@property
|
|
450
|
+
def CurrentCamera(self) -> CameraClass | None:
|
|
451
|
+
if self.failed: return
|
|
452
|
+
|
|
453
|
+
addr = int.from_bytes(self.memory_module.read(self.instance.raw_address + Offsets["Camera"], 8), 'little')
|
|
454
|
+
if addr == 0:
|
|
455
|
+
return None
|
|
456
|
+
|
|
457
|
+
return CameraClass(self.memory_module, RBXInstance(addr, self.memory_module))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robloxmemoryapi
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python Library that abstracts reading data from the Roblox DataModel
|
|
5
|
+
Author-email: upio <notpoiu@users.noreply.github.com>, mstudio45 <mstudio45@users.noreply.github.com>, ActualMasterOogway <ActualMasterOogway@users.noreply.github.com>
|
|
6
|
+
License: Copyright 2025 upio, mstudio45, master oogway
|
|
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
|
+
|
|
14
|
+
Project-URL: Homepage, https://github.com/notpoiu/RobloxMemoryAPI
|
|
15
|
+
Project-URL: Issues, https://github.com/notpoiu/RobloxMemoryAPI/issues
|
|
16
|
+
Keywords: roblox,memory,windows
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
20
|
+
Classifier: Development Status :: 3 - Alpha
|
|
21
|
+
Classifier: Intended Audience :: Developers
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE.md
|
|
26
|
+
Requires-Dist: requests>=2.0
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# RobloxMemoryAPI
|
|
30
|
+
|
|
31
|
+
A Python library that is _hopefully stealthy_ and abstracts externally reading memory to get datamodel information from the roblox game client.
|
|
32
|
+
|
|
33
|
+
This was made by [upio](https://github.com/notpoiu), [mstudio45](https://github.com/mstudio45), and [Master Oogway](https://github.com/ActualMasterOogway) and created for the [Dig Macro](https://github.com/mstudio45/digmacro) project (external mode and not the computer vision mode).
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
PyPI:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install robloxmemoryapi
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Development (editable install from source):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
An example script can be found in [example.py](example.py). If running from the repo, use the editable install above so `import robloxmemoryapi` resolves the `src` package.
|
|
52
|
+
|
|
53
|
+
Import the library and create a client instance:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from robloxmemoryapi import RobloxGameClient
|
|
57
|
+
|
|
58
|
+
client = RobloxGameClient()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Access the data model:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
game = client.DataModel
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Get the local player's name:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
print("Player Name:", game.Players.LocalPlayer.Name)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
robloxmemoryapi/__init__.py,sha256=HdrWxS_cjqt7V7P5sL2N8_vGGLKV6xOxAZGSQL4nzIM,2560
|
|
2
|
+
robloxmemoryapi/data/offsets.json,sha256=VfVvOpFA11CxJgDgqqpjeRm-mwehSDqZM-e3TXCkS0s,79
|
|
3
|
+
robloxmemoryapi/utils/__init__.py,sha256=PtpwEbCo79OW68ycpVRjLg1DN-lW_3Z6bdMTIDMF4ek,52
|
|
4
|
+
robloxmemoryapi/utils/memory.py,sha256=yjLvvmhxcpQe8u_TULKDSmMiGM4gmYtj214JyGKDEcc,9534
|
|
5
|
+
robloxmemoryapi/utils/offsets.py,sha256=jWfl0ERDxO4-vGXN7FRkv4-Nz1x257pigqhMM06fWI4,1463
|
|
6
|
+
robloxmemoryapi/utils/rbx/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
robloxmemoryapi/utils/rbx/datastructures.py,sha256=U_tBET2RqoCllqFIb1Ry_ct0jx7YOTWjaTJLvH4htcA,8462
|
|
8
|
+
robloxmemoryapi/utils/rbx/instance.py,sha256=cRghPNhsRys9g8Xi1fe1dq1wkpefmDJmIWnqndQ-Szc,15644
|
|
9
|
+
robloxmemoryapi-0.0.1.dist-info/licenses/LICENSE.md,sha256=zngVZqcrtwUO0Un82JVGU24AS0_ly3IjE0ussEvqktM,1078
|
|
10
|
+
robloxmemoryapi-0.0.1.dist-info/METADATA,sha256=TBSMJ3O4tC0tmWNYadY1QyZcztB0umU_1hH8gkB8TV8,3186
|
|
11
|
+
robloxmemoryapi-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
robloxmemoryapi-0.0.1.dist-info/top_level.txt,sha256=j28W-HyH5eTKJLbNUc5EGb3KYgNG4MJ6jcOgEKrO_Uc,16
|
|
13
|
+
robloxmemoryapi-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 upio, mstudio45, master oogway
|
|
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
|
+
robloxmemoryapi
|