robloxmemoryapi 0.3.1__tar.gz → 0.3.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.
Files changed (29) hide show
  1. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/PKG-INFO +3 -2
  2. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/pyproject.toml +3 -2
  3. robloxmemoryapi-0.3.2/src/robloxmemoryapi/__init__.py +231 -0
  4. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/src/robloxmemoryapi/_native/memory.cpp +134 -2
  5. robloxmemoryapi-0.3.2/src/robloxmemoryapi/_version.py +44 -0
  6. robloxmemoryapi-0.3.2/src/robloxmemoryapi/utils/macos.py +153 -0
  7. robloxmemoryapi-0.3.2/src/robloxmemoryapi/utils/offsets.py +43 -0
  8. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/src/robloxmemoryapi/utils/rbx/instance.py +14 -3
  9. robloxmemoryapi-0.3.1/src/robloxmemoryapi/__init__.py +0 -112
  10. robloxmemoryapi-0.3.1/src/robloxmemoryapi/utils/offsets.py +0 -24
  11. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/.github/workflows/publish.yml +0 -0
  12. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/.gitignore +0 -0
  13. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/.luaurc +0 -0
  14. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/CMakeLists.txt +0 -0
  15. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/LICENSE.md +0 -0
  16. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/README.md +0 -0
  17. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/example.py +0 -0
  18. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/misc/BytecodeGen.luau +0 -0
  19. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/rokit.toml +0 -0
  20. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/src/robloxmemoryapi/_native/__init__.py +0 -0
  21. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/src/robloxmemoryapi/utils/__init__.py +0 -0
  22. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/src/robloxmemoryapi/utils/luau/__init__.py +0 -0
  23. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/src/robloxmemoryapi/utils/luau/parser.py +0 -0
  24. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/src/robloxmemoryapi/utils/memory.py +0 -0
  25. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/src/robloxmemoryapi/utils/rbx/__init__.py +0 -0
  26. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/src/robloxmemoryapi/utils/rbx/bytecode/decryptor.py +0 -0
  27. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/src/robloxmemoryapi/utils/rbx/bytecode/encryptor.py +0 -0
  28. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/src/robloxmemoryapi/utils/rbx/datastructures.py +0 -0
  29. {robloxmemoryapi-0.3.1 → robloxmemoryapi-0.3.2}/src/robloxmemoryapi/utils/rbx/fflags.py +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: robloxmemoryapi
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Python Library that abstracts reading and writing data from the Roblox DataModel
5
- Keywords: roblox,memory,windows
5
+ Keywords: roblox,memory,windows,macOS
6
6
  Author-Email: upio <notpoiu@users.noreply.github.com>, mstudio45 <mstudio45@users.noreply.github.com>, ActualMasterOogway <ActualMasterOogway@users.noreply.github.com>
7
7
  License: Copyright 2025 upio, mstudio45, master oogway
8
8
 
@@ -15,6 +15,7 @@ License: Copyright 2025 upio, mstudio45, master oogway
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Operating System :: MacOS
18
19
  Classifier: Development Status :: 3 - Alpha
19
20
  Classifier: Intended Audience :: Developers
20
21
  Classifier: Topic :: Software Development :: Libraries
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "robloxmemoryapi"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Python Library that abstracts reading and writing data from the Roblox DataModel"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.9"
@@ -14,11 +14,12 @@ authors = [
14
14
  { name = "mstudio45", email = "mstudio45@users.noreply.github.com" },
15
15
  { name = "ActualMasterOogway", email = "ActualMasterOogway@users.noreply.github.com" },
16
16
  ]
17
- keywords = ["roblox", "memory", "windows"]
17
+ keywords = ["roblox", "memory", "windows", "macOS"]
18
18
  classifiers = [
19
19
  "Programming Language :: Python :: 3",
20
20
  "License :: OSI Approved :: MIT License",
21
21
  "Operating System :: Microsoft :: Windows",
22
+ "Operating System :: MacOS",
22
23
  "Development Status :: 3 - Alpha",
23
24
  "Intended Audience :: Developers",
24
25
  "Topic :: Software Development :: Libraries",
@@ -0,0 +1,231 @@
1
+ import platform
2
+ import math
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+
8
+ from ._version import __version__
9
+
10
+ __all__ = [
11
+ "RobloxRandom",
12
+ "RobloxGameClient",
13
+ "codesign_roblox_macos",
14
+ "codesign_python_macos",
15
+ "__version__",
16
+ ]
17
+
18
+
19
+ def codesign_roblox_macos(
20
+ app_path: str | os.PathLike[str] | None = None,
21
+ *,
22
+ silicon_app_path: str | os.PathLike[str] = "/Applications/Roblox.app",
23
+ intel_app_path: str | os.PathLike[str] = "/Applications/RobloxPlayer.app",
24
+ use_sudo: bool = True,
25
+ ) -> str:
26
+ """Ad-hoc sign a macOS Roblox app bundle for local memory access workflows."""
27
+ if platform.system() != "Darwin":
28
+ raise RuntimeError("codesign_roblox_macos is only available on macOS.")
29
+
30
+ if app_path is None:
31
+ app_path = silicon_app_path if platform.machine().lower() == "arm64" else intel_app_path
32
+
33
+ app_path = os.fspath(app_path)
34
+ if not os.path.exists(app_path):
35
+ raise FileNotFoundError(app_path)
36
+
37
+ sudo_prefix = ["sudo"] if use_sudo and os.geteuid() != 0 else []
38
+ subprocess.run(
39
+ sudo_prefix + ["codesign", "--remove-signature", app_path],
40
+ stdout=subprocess.DEVNULL,
41
+ stderr=subprocess.DEVNULL,
42
+ check=False,
43
+ )
44
+ subprocess.run(
45
+ sudo_prefix + ["codesign", "--force", "--deep", "--sign", "-", app_path],
46
+ check=True,
47
+ )
48
+
49
+ return app_path
50
+
51
+
52
+ def codesign_python_macos(
53
+ executable_path: str | os.PathLike[str] | None = None,
54
+ *,
55
+ use_sudo: bool = True,
56
+ ) -> str:
57
+ """Ad-hoc sign the Python executable/launcher with the macOS debugger entitlement."""
58
+ if platform.system() != "Darwin":
59
+ raise RuntimeError("codesign_python_macos is only available on macOS.")
60
+
61
+ if executable_path is None:
62
+ executable_path = sys.executable
63
+
64
+ executable_path = os.path.realpath(os.fspath(executable_path))
65
+ if not os.path.exists(executable_path):
66
+ raise FileNotFoundError(executable_path)
67
+
68
+ entitlements = """<?xml version="1.0" encoding="UTF-8"?>
69
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
70
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
71
+ <plist version="1.0">
72
+ <dict>
73
+ <key>com.apple.security.cs.debugger</key>
74
+ <true/>
75
+ <key>com.apple.security.get-task-allow</key>
76
+ <true/>
77
+ <key>SecTaskAccess</key>
78
+ <array><string>allowed</string></array>
79
+ </dict>
80
+ </plist>
81
+ """
82
+
83
+ sudo_prefix = ["sudo"] if use_sudo and os.geteuid() != 0 else []
84
+ with tempfile.NamedTemporaryFile("w", suffix=".plist", delete=False) as entitlement_file:
85
+ entitlement_file.write(entitlements)
86
+ entitlement_path = entitlement_file.name
87
+
88
+ try:
89
+ subprocess.run(
90
+ sudo_prefix + ["codesign", "--remove-signature", executable_path],
91
+ stdout=subprocess.DEVNULL,
92
+ stderr=subprocess.DEVNULL,
93
+ check=False,
94
+ )
95
+ subprocess.run(
96
+ sudo_prefix
97
+ + [
98
+ "codesign",
99
+ "--force",
100
+ "--sign",
101
+ "-",
102
+ "--entitlements",
103
+ entitlement_path,
104
+ executable_path,
105
+ ],
106
+ check=True,
107
+ )
108
+ finally:
109
+ try:
110
+ os.unlink(entitlement_path)
111
+ except OSError:
112
+ pass
113
+
114
+ return executable_path
115
+
116
+
117
+ class RobloxRandom:
118
+ MULT = 6364136223846793005
119
+ INC = 105
120
+ MASK64 = (1 << 64) - 1
121
+
122
+ def __init__(self, seed):
123
+ s = math.floor(seed)
124
+
125
+ self._state = 0
126
+ self._inc = RobloxRandom.INC
127
+ self._next_internal() # warm-up #1
128
+ self._state = (self._state + s) & RobloxRandom.MASK64
129
+ self._next_internal() # warm-up #2
130
+
131
+ def _next_internal(self):
132
+ old = self._state
133
+ self._state = (old * RobloxRandom.MULT + self._inc) & RobloxRandom.MASK64
134
+ x = ((old >> 18) ^ old) >> 27
135
+ r = old >> 59
136
+ return ((x >> r) | (x << ((32 - r) & 31))) & 0xFFFFFFFF
137
+
138
+ def _next_fraction64(self):
139
+ lo = self._next_internal()
140
+ hi = self._next_internal()
141
+ bits = (hi << 32) | lo
142
+ return bits / 2**64
143
+
144
+ def NextNumber(self, minimum=0.0, maximum=1.0):
145
+ frac = self._next_fraction64()
146
+ return minimum + frac * (maximum - minimum)
147
+
148
+ def NextInteger(self, a, b=None):
149
+ if b is None:
150
+ u = a
151
+ r = self._next_internal()
152
+ return ((u * r) >> 32) + 1
153
+ else:
154
+ lo, hi = (a, b) if a <= b else (b, a)
155
+ u = hi - lo + 1
156
+ r = self._next_internal()
157
+ return ((u * r) >> 32) + lo
158
+
159
+ class RobloxGameClient:
160
+ def __init__(
161
+ self,
162
+ pid: int = None,
163
+ process_name: str = "RobloxPlayerBeta.exe",
164
+ allow_write: bool = False,
165
+ ):
166
+ system = platform.system()
167
+ if system not in {"Windows", "Darwin"}:
168
+ self.failed = True
169
+ return
170
+
171
+ if system == "Darwin" and os.geteuid() != 0:
172
+ raise PermissionError(
173
+ "macOS memory access requires running the Python process with sudo/root. "
174
+ "Run this script with sudo and try again."
175
+ )
176
+
177
+ from .utils.memory import (
178
+ EvasiveProcess,
179
+ PROCESS_QUERY_INFORMATION,
180
+ PROCESS_VM_READ,
181
+ PROCESS_VM_WRITE,
182
+ PROCESS_VM_OPERATION,
183
+ get_pid_by_name,
184
+ )
185
+
186
+ if system == "Darwin" and process_name == "RobloxPlayerBeta.exe":
187
+ process_name = "RobloxPlayer"
188
+
189
+ if pid is None:
190
+ self.pid = get_pid_by_name(process_name)
191
+ else:
192
+ self.pid = pid
193
+
194
+ if self.pid is None or self.pid == 0:
195
+ raise ValueError("Failed to get PID.")
196
+
197
+ if system == "Darwin":
198
+ os.environ["ROBLOXMEMORYAPI_ROBLOX_PID"] = str(self.pid)
199
+
200
+ desired_access = PROCESS_VM_READ | PROCESS_QUERY_INFORMATION
201
+ if allow_write:
202
+ desired_access |= PROCESS_VM_WRITE | PROCESS_VM_OPERATION
203
+
204
+ self.memory_module = EvasiveProcess(self.pid, desired_access)
205
+ self.failed = False
206
+ self._fflags = None
207
+
208
+ def close(self):
209
+ self.memory_module.close()
210
+
211
+ @property
212
+ def FFlags(self):
213
+ if platform.system() not in {"Windows", "Darwin"}:
214
+ raise RuntimeError("This module is only compatible with Windows and macOS.")
215
+ elif self.failed:
216
+ raise RuntimeError("There was an error while getting access to memory. Please try again later.")
217
+
218
+ if self._fflags is None:
219
+ from .utils.rbx.fflags import FFlagManager
220
+ self._fflags = FFlagManager(self.memory_module)
221
+ return self._fflags
222
+
223
+ @property
224
+ def DataModel(self):
225
+ if platform.system() not in {"Windows", "Darwin"}:
226
+ raise RuntimeError("This module is only compatible with Windows and macOS.")
227
+ elif self.failed:
228
+ raise RuntimeError("There was an error while getting access to memory. Please try again later.")
229
+
230
+ from .utils.rbx.instance import DataModel
231
+ return DataModel(self.memory_module)
@@ -2,9 +2,11 @@
2
2
  #include <pybind11/stl.h>
3
3
 
4
4
  #include <algorithm>
5
+ #include <cctype>
5
6
  #include <cstdint>
6
7
  #include <cstring>
7
8
  #include <iomanip>
9
+ #include <optional>
8
10
  #include <sstream>
9
11
  #include <stdexcept>
10
12
  #include <string>
@@ -26,6 +28,7 @@ namespace py = pybind11;
26
28
  #include <mach-o/loader.h>
27
29
  #include <libproc.h>
28
30
  #include <sys/proc_info.h>
31
+ #include <unistd.h>
29
32
  #endif
30
33
 
31
34
  namespace {
@@ -39,6 +42,17 @@ constexpr std::uint32_t MEM_RESERVE_VALUE = 0x2000;
39
42
  constexpr std::uint32_t PAGE_READWRITE_VALUE = 0x04;
40
43
  constexpr std::uint32_t PAGE_EXECUTE_READWRITE_VALUE = 0x40;
41
44
 
45
+ #if defined(__APPLE__)
46
+ constexpr std::string_view MACOS_CODESIGN_HINT =
47
+ "\n\nTry ad-hoc codesigning Roblox and the Python executable/launcher, "
48
+ "from a sudo/root shell run the following commands:\n"
49
+ "from robloxmemoryapi import codesign_roblox_macos, codesign_python_macos\n\n"
50
+ "# run with sudo\n"
51
+ "codesign_python_macos()\n"
52
+ "codesign_roblox_macos()\n\n"
53
+ "Then restart roblox and run the script again.";
54
+ #endif
55
+
42
56
  std::string hex_status(long status) {
43
57
  std::ostringstream oss;
44
58
  oss << "0x" << std::uppercase << std::hex << static_cast<unsigned long>(status);
@@ -276,13 +290,22 @@ public:
276
290
  raise_python(PyExc_ConnectionError, "Failed to get module base address.");
277
291
  }
278
292
  #elif defined(__APPLE__)
293
+ if (geteuid() != 0) {
294
+ raise_python(
295
+ PyExc_PermissionError,
296
+ "macOS memory access requires running the Python process with sudo/root. "
297
+ "Run this script with sudo and try again."
298
+ );
299
+ }
300
+
279
301
  mach_port_t task = MACH_PORT_NULL;
280
302
  kern_return_t result = task_for_pid(mach_task_self(), pid, &task);
281
303
  if (result != KERN_SUCCESS) {
282
304
  raise_python(
283
305
  PyExc_OSError,
284
306
  "task_for_pid failed: " + std::string(mach_error_string(result)) +
285
- ". macOS memory reads require permission to acquire the target task port."
307
+ ". macOS memory reads require permission to acquire the target task port." +
308
+ std::string(MACOS_CODESIGN_HINT)
286
309
  );
287
310
  }
288
311
 
@@ -535,6 +558,104 @@ public:
535
558
  return py::reinterpret_steal<py::str>(decoded).cast<std::string>();
536
559
  }
537
560
 
561
+ #if defined(__APPLE__)
562
+ static bool is_plausible_string(const std::string &value) {
563
+ if (value.empty()) {
564
+ return false;
565
+ }
566
+
567
+ return std::all_of(value.begin(), value.end(), [](const unsigned char c) {
568
+ return c == '\t' || c == '\n' || c == '\r' || std::isprint(c);
569
+ });
570
+ }
571
+
572
+ std::optional<std::string> read_libcpp_string(std::uintptr_t address) {
573
+ const std::string bytes = read_bytes(address, 24);
574
+ if (bytes.size() != 24) {
575
+ return std::nullopt;
576
+ }
577
+
578
+ auto read_qword = [&](const std::size_t offset) {
579
+ std::uintptr_t value = 0;
580
+ std::memcpy(&value, bytes.data() + offset, sizeof(value));
581
+ return value;
582
+ };
583
+
584
+ auto read_inline = [&](const std::size_t data_offset, const std::size_t size)
585
+ -> std::optional<std::string> {
586
+ if (size == 0 || data_offset + size > bytes.size()) {
587
+ return std::nullopt;
588
+ }
589
+
590
+ std::string value(bytes.data() + data_offset, size);
591
+ if (!is_plausible_string(value)) {
592
+ return std::nullopt;
593
+ }
594
+
595
+ return value;
596
+ };
597
+
598
+ // libc++ short layout used by current Roblox macOS builds stores raw size in byte 0.
599
+ if (static_cast<unsigned char>(bytes[0]) <= 23) {
600
+ if (const auto value = read_inline(1, static_cast<unsigned char>(bytes[0]))) {
601
+ return value;
602
+ }
603
+ }
604
+
605
+ // Some libc++ layouts shift the short size left by one.
606
+ if ((static_cast<unsigned char>(bytes[0]) & 1U) == 0 &&
607
+ (static_cast<unsigned char>(bytes[0]) >> 1) <= 23) {
608
+ if (const auto value = read_inline(1, static_cast<unsigned char>(bytes[0]) >> 1)) {
609
+ return value;
610
+ }
611
+ }
612
+
613
+ // Alternate libc++ layout: short data begins at byte 0 and size is stored in byte 23.
614
+ if (static_cast<unsigned char>(bytes[23]) <= 23) {
615
+ if (const auto value = read_inline(0, static_cast<unsigned char>(bytes[23]))) {
616
+ return value;
617
+ }
618
+ }
619
+
620
+ if ((static_cast<unsigned char>(bytes[23]) & 1U) == 0 &&
621
+ (static_cast<unsigned char>(bytes[23]) >> 1) <= 23) {
622
+ if (const auto value = read_inline(0, static_cast<unsigned char>(bytes[23]) >> 1)) {
623
+ return value;
624
+ }
625
+ }
626
+
627
+ auto read_external = [&](std::uintptr_t data_ptr, std::uintptr_t size)
628
+ -> std::optional<std::string> {
629
+ if (data_ptr < 0x10000 || size == 0 || size > 4096) {
630
+ return std::nullopt;
631
+ }
632
+
633
+ std::string value = read_raw_string(data_ptr, static_cast<std::size_t>(size + 1));
634
+ if (value.size() != size || !is_plausible_string(value)) {
635
+ return std::nullopt;
636
+ }
637
+
638
+ return value;
639
+ };
640
+
641
+ // libc++ default long layout: cap, size, data.
642
+ if (static_cast<unsigned char>(bytes[0]) & 1U) {
643
+ if (const auto value = read_external(read_qword(16), read_qword(8))) {
644
+ return value;
645
+ }
646
+ }
647
+
648
+ // libc++ alternate long layout: data, size, cap.
649
+ if (static_cast<unsigned char>(bytes[23]) & 0x80U) {
650
+ if (const auto value = read_external(read_qword(0), read_qword(8))) {
651
+ return value;
652
+ }
653
+ }
654
+
655
+ return std::nullopt;
656
+ }
657
+ #endif
658
+
538
659
  void write_raw_string(std::uintptr_t address, const std::string &value, bool null_terminate = true) {
539
660
  std::string bytes = value;
540
661
  if (null_terminate) {
@@ -586,6 +707,13 @@ public:
586
707
 
587
708
  std::string read_string(std::uintptr_t address, std::uintptr_t offset = 0) {
588
709
  address += offset;
710
+
711
+ #if defined(__APPLE__)
712
+ if (const auto value = read_libcpp_string(address)) {
713
+ return *value;
714
+ }
715
+ #endif
716
+
589
717
  const int string_length = read_int(address + 0x10);
590
718
 
591
719
  if (string_length <= 0 || string_length > 1024 * 1024) {
@@ -682,7 +810,11 @@ private:
682
810
  }
683
811
 
684
812
  if (result != KERN_SUCCESS) {
685
- raise_python(PyExc_OSError, "mach_vm_read_overwrite failed: " + std::string(mach_error_string(result)));
813
+ raise_python(
814
+ PyExc_OSError,
815
+ "mach_vm_read_overwrite failed: " + std::string(mach_error_string(result)) +
816
+ std::string(MACOS_CODESIGN_HINT)
817
+ );
686
818
  }
687
819
 
688
820
  buffer.resize(static_cast<std::size_t>(bytes_read));
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib import metadata
4
+ from pathlib import Path
5
+ import re
6
+
7
+ _DISTRIBUTION_NAME = "robloxmemoryapi"
8
+
9
+
10
+ def _read_source_version() -> str | None:
11
+ for parent in Path(__file__).resolve().parents:
12
+ pyproject_path = parent / "pyproject.toml"
13
+ if not pyproject_path.is_file():
14
+ continue
15
+
16
+ in_project_section = False
17
+ for line in pyproject_path.read_text(encoding="utf-8").splitlines():
18
+ stripped = line.strip()
19
+ if stripped == "[project]":
20
+ in_project_section = True
21
+ continue
22
+ if in_project_section and stripped.startswith("["):
23
+ break
24
+ if in_project_section:
25
+ match = re.match(r'version\s*=\s*["\']([^"\']+)["\']', stripped)
26
+ if match:
27
+ return match.group(1)
28
+
29
+ return None
30
+
31
+
32
+ def _read_installed_version() -> str | None:
33
+ try:
34
+ return metadata.version(_DISTRIBUTION_NAME)
35
+ except metadata.PackageNotFoundError:
36
+ return None
37
+
38
+
39
+ def _resolve_version() -> str:
40
+ return _read_source_version() or _read_installed_version() or "0+unknown"
41
+
42
+
43
+ __version__ = _resolve_version()
44
+ USER_AGENT = f"RobloxMemoryAPI/{__version__}"
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import os
5
+ import platform
6
+ import subprocess
7
+
8
+
9
+ def normalize_architecture(architecture: str | None) -> str | None:
10
+ if not architecture:
11
+ return None
12
+
13
+ normalized = architecture.strip().lower()
14
+ if normalized in {"arm", "arm64", "aarch64", "apple silicon", "silicon"}:
15
+ return "arm64"
16
+ if normalized in {"intel", "x64", "x86_64", "amd64"}:
17
+ return "x64"
18
+ return None
19
+
20
+
21
+ def process_path(pid: int) -> str | None:
22
+ if platform.system() != "Darwin" or pid <= 0:
23
+ return None
24
+
25
+ try:
26
+ libc = ctypes.CDLL(None)
27
+ proc_pidpath = libc.proc_pidpath
28
+ proc_pidpath.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_uint32]
29
+ proc_pidpath.restype = ctypes.c_int
30
+
31
+ buffer = ctypes.create_string_buffer(4096)
32
+ result = proc_pidpath(pid, buffer, ctypes.sizeof(buffer))
33
+ if result > 0:
34
+ return os.fsdecode(buffer.value)
35
+ except Exception:
36
+ pass
37
+
38
+ try:
39
+ output = subprocess.check_output(
40
+ ["ps", "-p", str(pid), "-o", "comm="],
41
+ stderr=subprocess.DEVNULL,
42
+ text=True,
43
+ ).strip()
44
+ return output or None
45
+ except Exception:
46
+ return None
47
+
48
+
49
+ def binary_architecture(path: str | os.PathLike[str] | None) -> str | None:
50
+ if not path:
51
+ return None
52
+
53
+ path = os.fspath(path)
54
+ if not os.path.exists(path):
55
+ return None
56
+
57
+ try:
58
+ output = subprocess.check_output(
59
+ ["lipo", "-archs", path],
60
+ stderr=subprocess.DEVNULL,
61
+ text=True,
62
+ ).strip().lower()
63
+ architectures = set(output.split())
64
+ if architectures == {"arm64"}:
65
+ return "arm64"
66
+ if architectures <= {"x86_64", "i386"} and "x86_64" in architectures:
67
+ return "x64"
68
+ except Exception:
69
+ pass
70
+
71
+ try:
72
+ output = subprocess.check_output(
73
+ ["file", "-b", path],
74
+ stderr=subprocess.DEVNULL,
75
+ text=True,
76
+ ).lower()
77
+ if "arm64" in output and "x86_64" not in output:
78
+ return "arm64"
79
+ if "x86_64" in output and "arm64" not in output:
80
+ return "x64"
81
+ except Exception:
82
+ pass
83
+
84
+ return None
85
+
86
+
87
+ def running_roblox_binary_path() -> str | None:
88
+ try:
89
+ output = subprocess.check_output(
90
+ [
91
+ "pgrep",
92
+ "-f",
93
+ "/Applications/.*Roblox.*\\.app/Contents/MacOS/RobloxPlayer",
94
+ ],
95
+ stderr=subprocess.DEVNULL,
96
+ text=True,
97
+ )
98
+ except Exception:
99
+ return None
100
+
101
+ for line in output.splitlines():
102
+ try:
103
+ pid = int(line.strip())
104
+ except ValueError:
105
+ continue
106
+
107
+ path = process_path(pid)
108
+ if path:
109
+ return path
110
+
111
+ return None
112
+
113
+
114
+ def installed_roblox_architecture() -> str | None:
115
+ candidates = [
116
+ "/Applications/Roblox.app/Contents/MacOS/RobloxPlayer",
117
+ "/Applications/RobloxPlayer.app/Contents/MacOS/RobloxPlayer",
118
+ ]
119
+
120
+ detected = []
121
+ for path in candidates:
122
+ architecture = binary_architecture(path)
123
+ if architecture:
124
+ detected.append(architecture)
125
+
126
+ if len(set(detected)) == 1:
127
+ return detected[0]
128
+ return None
129
+
130
+
131
+ def roblox_architecture() -> str:
132
+ env_architecture = normalize_architecture(os.environ.get("ROBLOXMEMORYAPI_ROBLOX_ARCH"))
133
+ if env_architecture:
134
+ return env_architecture
135
+
136
+ try:
137
+ pid = int(os.environ.get("ROBLOXMEMORYAPI_ROBLOX_PID", "0"))
138
+ except ValueError:
139
+ pid = 0
140
+
141
+ architecture = binary_architecture(process_path(pid))
142
+ if architecture:
143
+ return architecture
144
+
145
+ architecture = binary_architecture(running_roblox_binary_path())
146
+ if architecture:
147
+ return architecture
148
+
149
+ architecture = installed_roblox_architecture()
150
+ if architecture:
151
+ return architecture
152
+
153
+ return "arm64" if platform.machine().lower() in {"arm64", "aarch64"} else "x64"
@@ -0,0 +1,43 @@
1
+ import requests
2
+ import platform
3
+
4
+ from .._version import USER_AGENT
5
+ from .macos import roblox_architecture
6
+
7
+ IMTHEO_BASE_URL = "https://offsets.imtheo.lol"
8
+ MAC_ARM_OFFSETS_URL = "https://offsets.upio.dev/rbxl-macarm-latest.json"
9
+ MAC_INTEL_OFFSETS_URL = "https://offsets.upio.dev/rbxl-macintel-latest.json"
10
+ REQUEST_HEADERS = {"User-Agent": USER_AGENT}
11
+
12
+ _macos_roblox_architecture = roblox_architecture
13
+
14
+
15
+ def _offsets_url() -> str:
16
+ if platform.system() != "Darwin":
17
+ return f"{IMTHEO_BASE_URL}/Offsets.json"
18
+
19
+ if roblox_architecture() == "arm64":
20
+ return MAC_ARM_OFFSETS_URL
21
+
22
+ return MAC_INTEL_OFFSETS_URL
23
+
24
+ # Offsets
25
+ OffsetsRequest = requests.get(_offsets_url(), headers=REQUEST_HEADERS)
26
+
27
+ try:
28
+ Offsets = OffsetsRequest.json()["Offsets"]
29
+ except Exception:
30
+ Offsets = {}
31
+
32
+ # FFlag offsets (lazily loaded)
33
+ _fflag_data = None
34
+ _fflag_offsets = None
35
+
36
+ def get_fflag_offsets() -> dict:
37
+ global _fflag_data, _fflag_offsets
38
+ if _fflag_data is None:
39
+ resp = requests.get(f"{IMTHEO_BASE_URL}/FFlags.json", headers=REQUEST_HEADERS)
40
+ resp.raise_for_status()
41
+ _fflag_data = resp.json()
42
+ _fflag_offsets = _fflag_data["FFlagOffsets"]["FFlags"]
43
+ return _fflag_offsets
@@ -70,7 +70,7 @@ _ENABLED_OFFSETS_BY_CLASS = {
70
70
  "BloomEffect": bloom_effect_offsets,
71
71
  "DepthOfFieldEffect": depth_of_field_effect_offsets,
72
72
  "SunRaysEffect": sun_rays_effect_offsets,
73
- "ScreenGui": {"Enabled": gui_offsets["ScreenGui_Enabled"]},
73
+ "ScreenGui": {"Enabled": gui_offsets.get("ScreenGui_Enabled")},
74
74
  "ProximityPrompt": proximityprompt_offsets,
75
75
  "Tool": tool_offsets,
76
76
  "SpawnLocation": spawnlocation_offsets,
@@ -295,6 +295,12 @@ class RBXInstance:
295
295
  f"{property_name} is only available on {self._format_class_list(offsets_by_class)} instances."
296
296
  )
297
297
  return None
298
+ if offsets.get(property_name) is None:
299
+ if write:
300
+ raise AttributeError(
301
+ f"{property_name} offset is not available for {self.ClassName} instances."
302
+ )
303
+ return None
298
304
  return offsets
299
305
 
300
306
  def _read_class_float(self, property_name, offsets_by_class):
@@ -4632,8 +4638,13 @@ class DataModel(ServiceBase):
4632
4638
 
4633
4639
  with self._refresh_lock:
4634
4640
  try:
4635
- fake_datamodel_ptr = self.memory_module.get_address(Offsets["FakeDataModel"]["Pointer"], pointer=True)
4636
- datamodel_address_ptr = self.memory_module.get_pointer(fake_datamodel_ptr, Offsets["FakeDataModel"]["RealDataModel"])
4641
+ visual_engine = VisualEngine(self.memory_module)
4642
+ fake_datamodel_ptr = visual_engine.FakeDataModel if not visual_engine.failed else 0
4643
+ datamodel_address_ptr = (
4644
+ self.memory_module.get_pointer(fake_datamodel_ptr, Offsets["FakeDataModel"]["RealDataModel"])
4645
+ if fake_datamodel_ptr != 0
4646
+ else 0
4647
+ )
4637
4648
 
4638
4649
  if datamodel_address_ptr == 0:
4639
4650
  if self.instance is not None:
@@ -1,112 +0,0 @@
1
- import platform
2
- import math
3
-
4
- __all__ = ["RobloxRandom", "RobloxGameClient", "__version__"]
5
- __version__ = "0.0.2"
6
-
7
- class RobloxRandom:
8
- MULT = 6364136223846793005
9
- INC = 105
10
- MASK64 = (1 << 64) - 1
11
-
12
- def __init__(self, seed):
13
- s = math.floor(seed)
14
-
15
- self._state = 0
16
- self._inc = RobloxRandom.INC
17
- self._next_internal() # warm-up #1
18
- self._state = (self._state + s) & RobloxRandom.MASK64
19
- self._next_internal() # warm-up #2
20
-
21
- def _next_internal(self):
22
- old = self._state
23
- self._state = (old * RobloxRandom.MULT + self._inc) & RobloxRandom.MASK64
24
- x = ((old >> 18) ^ old) >> 27
25
- r = old >> 59
26
- return ((x >> r) | (x << ((32 - r) & 31))) & 0xFFFFFFFF
27
-
28
- def _next_fraction64(self):
29
- lo = self._next_internal()
30
- hi = self._next_internal()
31
- bits = (hi << 32) | lo
32
- return bits / 2**64
33
-
34
- def NextNumber(self, minimum=0.0, maximum=1.0):
35
- frac = self._next_fraction64()
36
- return minimum + frac * (maximum - minimum)
37
-
38
- def NextInteger(self, a, b=None):
39
- if b is None:
40
- u = a
41
- r = self._next_internal()
42
- return ((u * r) >> 32) + 1
43
- else:
44
- lo, hi = (a, b) if a <= b else (b, a)
45
- u = hi - lo + 1
46
- r = self._next_internal()
47
- return ((u * r) >> 32) + lo
48
-
49
- class RobloxGameClient:
50
- def __init__(
51
- self,
52
- pid: int = None,
53
- process_name: str = "RobloxPlayerBeta.exe",
54
- allow_write: bool = False,
55
- ):
56
- system = platform.system()
57
- if system not in {"Windows", "Darwin"}:
58
- self.failed = True
59
- return
60
-
61
- from .utils.memory import (
62
- EvasiveProcess,
63
- PROCESS_QUERY_INFORMATION,
64
- PROCESS_VM_READ,
65
- PROCESS_VM_WRITE,
66
- PROCESS_VM_OPERATION,
67
- get_pid_by_name,
68
- )
69
-
70
- if system == "Darwin" and process_name == "RobloxPlayerBeta.exe":
71
- process_name = "RobloxPlayer"
72
-
73
- if pid is None:
74
- self.pid = get_pid_by_name(process_name)
75
- else:
76
- self.pid = pid
77
-
78
- if self.pid is None or self.pid == 0:
79
- raise ValueError("Failed to get PID.")
80
-
81
- desired_access = PROCESS_VM_READ | PROCESS_QUERY_INFORMATION
82
- if allow_write:
83
- desired_access |= PROCESS_VM_WRITE | PROCESS_VM_OPERATION
84
-
85
- self.memory_module = EvasiveProcess(self.pid, desired_access)
86
- self.failed = False
87
- self._fflags = None
88
-
89
- def close(self):
90
- self.memory_module.close()
91
-
92
- @property
93
- def FFlags(self):
94
- if platform.system() not in {"Windows", "Darwin"}:
95
- raise RuntimeError("This module is only compatible with Windows and macOS.")
96
- elif self.failed:
97
- raise RuntimeError("There was an error while getting access to memory. Please try again later.")
98
-
99
- if self._fflags is None:
100
- from .utils.rbx.fflags import FFlagManager
101
- self._fflags = FFlagManager(self.memory_module)
102
- return self._fflags
103
-
104
- @property
105
- def DataModel(self):
106
- if platform.system() not in {"Windows", "Darwin"}:
107
- raise RuntimeError("This module is only compatible with Windows and macOS.")
108
- elif self.failed:
109
- raise RuntimeError("There was an error while getting access to memory. Please try again later.")
110
-
111
- from .utils.rbx.instance import DataModel
112
- return DataModel(self.memory_module)
@@ -1,24 +0,0 @@
1
- import requests
2
-
3
- BASE_URL = "https://offsets.imtheo.lol"
4
-
5
- # Offsets
6
- OffsetsRequest = requests.get(f"{BASE_URL}/Offsets.json")
7
-
8
- try:
9
- Offsets = OffsetsRequest.json()["Offsets"]
10
- except:
11
- Offsets = {}
12
-
13
- # FFlag offsets (lazily loaded)
14
- _fflag_data = None
15
- _fflag_offsets = None
16
-
17
- def get_fflag_offsets() -> dict:
18
- global _fflag_data, _fflag_offsets
19
- if _fflag_data is None:
20
- resp = requests.get(f"{BASE_URL}/FFlags.json")
21
- resp.raise_for_status()
22
- _fflag_data = resp.json()
23
- _fflag_offsets = _fflag_data["FFlagOffsets"]["FFlags"]
24
- return _fflag_offsets
File without changes