robloxmemoryapi 0.0.2__tar.gz → 0.0.4__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 (19) hide show
  1. {robloxmemoryapi-0.0.2/src/robloxmemoryapi.egg-info → robloxmemoryapi-0.0.4}/PKG-INFO +16 -2
  2. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/README.md +14 -0
  3. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/pyproject.toml +2 -2
  4. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/src/robloxmemoryapi/__init__.py +13 -2
  5. robloxmemoryapi-0.0.4/src/robloxmemoryapi/data/offsets.json +6 -0
  6. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/src/robloxmemoryapi/utils/memory.py +61 -2
  7. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/src/robloxmemoryapi/utils/offsets.py +2 -2
  8. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/src/robloxmemoryapi/utils/rbx/instance.py +289 -21
  9. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4/src/robloxmemoryapi.egg-info}/PKG-INFO +16 -2
  10. robloxmemoryapi-0.0.2/src/robloxmemoryapi/data/offsets.json +0 -6
  11. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/LICENSE.md +0 -0
  12. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/setup.cfg +0 -0
  13. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/src/robloxmemoryapi/utils/__init__.py +0 -0
  14. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/src/robloxmemoryapi/utils/rbx/__init__.py +0 -0
  15. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/src/robloxmemoryapi/utils/rbx/datastructures.py +0 -0
  16. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/src/robloxmemoryapi.egg-info/SOURCES.txt +0 -0
  17. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/src/robloxmemoryapi.egg-info/dependency_links.txt +0 -0
  18. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/src/robloxmemoryapi.egg-info/requires.txt +0 -0
  19. {robloxmemoryapi-0.0.2 → robloxmemoryapi-0.0.4}/src/robloxmemoryapi.egg-info/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robloxmemoryapi
3
- Version: 0.0.2
4
- Summary: Python Library that abstracts reading data from the Roblox DataModel
3
+ Version: 0.0.4
4
+ Summary: Python Library that abstracts reading and writing data from the Roblox DataModel
5
5
  Author-email: upio <notpoiu@users.noreply.github.com>, mstudio45 <mstudio45@users.noreply.github.com>, ActualMasterOogway <ActualMasterOogway@users.noreply.github.com>
6
6
  License: Copyright 2025 upio, mstudio45, master oogway
7
7
 
@@ -58,12 +58,26 @@ from robloxmemoryapi import RobloxGameClient
58
58
  client = RobloxGameClient()
59
59
  ```
60
60
 
61
+ To request write access (e.g. to patch values), pass `allow_write=True` when creating the client.
62
+
63
+ ```python
64
+ client = RobloxGameClient(allow_write=True)
65
+ ```
66
+
61
67
  Access the data model:
62
68
 
63
69
  ```python
64
70
  game = client.DataModel
65
71
  ```
66
72
 
73
+ Kill the local player:
74
+
75
+ ```python
76
+ # (requires allow_write=True when creating the client)
77
+ # allow_write may be detected by roblox. It is disabled by default.
78
+ game.Players.LocalPlayer.Character.Humanoid.Health = 0
79
+ ```
80
+
67
81
  Get the local player's name:
68
82
 
69
83
  ```python
@@ -30,12 +30,26 @@ from robloxmemoryapi import RobloxGameClient
30
30
  client = RobloxGameClient()
31
31
  ```
32
32
 
33
+ To request write access (e.g. to patch values), pass `allow_write=True` when creating the client.
34
+
35
+ ```python
36
+ client = RobloxGameClient(allow_write=True)
37
+ ```
38
+
33
39
  Access the data model:
34
40
 
35
41
  ```python
36
42
  game = client.DataModel
37
43
  ```
38
44
 
45
+ Kill the local player:
46
+
47
+ ```python
48
+ # (requires allow_write=True when creating the client)
49
+ # allow_write may be detected by roblox. It is disabled by default.
50
+ game.Players.LocalPlayer.Character.Humanoid.Health = 0
51
+ ```
52
+
39
53
  Get the local player's name:
40
54
 
41
55
  ```python
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "robloxmemoryapi"
7
- version = "0.0.2"
8
- description = "Python Library that abstracts reading data from the Roblox DataModel"
7
+ version = "0.0.4"
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"
11
11
  license = { file = "LICENSE.md" }
@@ -47,7 +47,12 @@ class RobloxRandom:
47
47
  return ((u * r) >> 32) + lo
48
48
 
49
49
  class RobloxGameClient:
50
- def __init__(self, pid: int = None, process_name: str = "RobloxPlayerBeta.exe"):
50
+ def __init__(
51
+ self,
52
+ pid: int = None,
53
+ process_name: str = "RobloxPlayerBeta.exe",
54
+ allow_write: bool = False,
55
+ ):
51
56
  if platform.system() != "Windows":
52
57
  self.failed = True
53
58
  return
@@ -56,6 +61,8 @@ class RobloxGameClient:
56
61
  EvasiveProcess,
57
62
  PROCESS_QUERY_INFORMATION,
58
63
  PROCESS_VM_READ,
64
+ PROCESS_VM_WRITE,
65
+ PROCESS_VM_OPERATION,
59
66
  get_pid_by_name,
60
67
  )
61
68
 
@@ -67,7 +74,11 @@ class RobloxGameClient:
67
74
  if self.pid is None or self.pid == 0:
68
75
  raise ValueError("Failed to get PID.")
69
76
 
70
- self.memory_module = EvasiveProcess(self.pid, PROCESS_VM_READ | PROCESS_QUERY_INFORMATION)
77
+ desired_access = PROCESS_VM_READ | PROCESS_QUERY_INFORMATION
78
+ if allow_write:
79
+ desired_access |= PROCESS_VM_WRITE | PROCESS_VM_OPERATION
80
+
81
+ self.memory_module = EvasiveProcess(self.pid, desired_access)
71
82
  self.failed = False
72
83
 
73
84
  def close(self):
@@ -0,0 +1,6 @@
1
+ {
2
+ "Text": "0xC10",
3
+ "Character": "0x360",
4
+ "PrimaryPart": "0x248"
5
+ }
6
+
@@ -20,6 +20,8 @@ BOOL = wintypes.BOOL
20
20
 
21
21
  PROCESS_QUERY_INFORMATION = 0x0400
22
22
  PROCESS_VM_READ = 0x0010
23
+ PROCESS_VM_WRITE = 0x0020
24
+ PROCESS_VM_OPERATION = 0x0008
23
25
  LIST_MODULES_ALL = 0x03
24
26
  STATUS_SUCCESS = 0
25
27
  MEM_COMMIT = 0x1000
@@ -79,17 +81,27 @@ NtReadVirtualMemoryProto = ctypes.WINFUNCTYPE(
79
81
  ctypes.c_ulong,
80
82
  ctypes.POINTER(ctypes.c_ulong)
81
83
  )
84
+ NtWriteVirtualMemoryProto = ctypes.WINFUNCTYPE(
85
+ NTSTATUS,
86
+ HANDLE,
87
+ LPVOID,
88
+ LPVOID,
89
+ ctypes.c_ulong,
90
+ ctypes.POINTER(ctypes.c_ulong)
91
+ )
82
92
  NtCloseProto = ctypes.WINFUNCTYPE(NTSTATUS, HANDLE)
83
93
 
84
94
  syscall_id_open = get_syscall_number("NtOpenProcess")
85
95
  syscall_id_read = get_syscall_number("NtReadVirtualMemory")
96
+ syscall_id_write = get_syscall_number("NtWriteVirtualMemory")
86
97
  syscall_id_close = get_syscall_number("NtClose")
87
98
 
88
- if not all([syscall_id_open, syscall_id_read, syscall_id_close]):
99
+ if not all([syscall_id_open, syscall_id_read, syscall_id_write, syscall_id_close]):
89
100
  raise RuntimeError("Could not find required syscall numbers.")
90
101
 
91
102
  nt_open_process_syscall = create_syscall_function(syscall_id_open, NtOpenProcessProto)
92
103
  nt_read_virtual_memory_syscall = create_syscall_function(syscall_id_read, NtReadVirtualMemoryProto)
104
+ nt_write_virtual_memory_syscall = create_syscall_function(syscall_id_write, NtWriteVirtualMemoryProto)
93
105
  nt_close_syscall = create_syscall_function(syscall_id_close, NtCloseProto)
94
106
 
95
107
  psapi.EnumProcessModulesEx.argtypes = [
@@ -204,6 +216,27 @@ class EvasiveProcess:
204
216
  raise OSError(f"NtReadVirtualMemory failed with NTSTATUS: 0x{status:X}")
205
217
  return buffer.raw[:bytes_read.value]
206
218
 
219
+ def write(self, address: int, data: bytes | bytearray) -> int:
220
+ if not self.handle or self.handle.value == 0:
221
+ raise ValueError("Process handle is not valid.")
222
+ if not isinstance(data, (bytes, bytearray)):
223
+ raise TypeError("data must be bytes-like.")
224
+ raw = bytes(data)
225
+ if len(raw) == 0:
226
+ return 0
227
+ buffer = (ctypes.c_ubyte * len(raw)).from_buffer_copy(raw)
228
+ bytes_written = ctypes.c_ulong(0)
229
+ status = nt_write_virtual_memory_syscall(
230
+ self.handle,
231
+ LPVOID(address),
232
+ buffer,
233
+ len(raw),
234
+ ctypes.byref(bytes_written)
235
+ )
236
+ if status != STATUS_SUCCESS:
237
+ raise OSError(f"NtWriteVirtualMemory failed with NTSTATUS: 0x{status:X}")
238
+ return bytes_written.value
239
+
207
240
  # numbers #
208
241
  def read_int(self, address: int) -> int:
209
242
  buffer = self.read(address, 4)
@@ -243,6 +276,23 @@ class EvasiveProcess:
243
276
  return floats
244
277
  except (OSError, struct.error) as e:
245
278
  return [0.0]
279
+
280
+ def write_int(self, address: int, value: int) -> None:
281
+ self.write(address, struct.pack('<I', value & 0xFFFFFFFF))
282
+
283
+ def write_long(self, address: int, value: int) -> None:
284
+ self.write(address, struct.pack('<Q', value & 0xFFFFFFFFFFFFFFFF))
285
+
286
+ def write_double(self, address: int, value: float) -> None:
287
+ self.write(address, struct.pack('<d', value))
288
+
289
+ def write_float(self, address: int, value: float) -> None:
290
+ self.write(address, struct.pack('<f', value))
291
+
292
+ def write_floats(self, address: int, values) -> None:
293
+ packed = b''.join(struct.pack('<f', float(v)) for v in values)
294
+ if packed:
295
+ self.write(address, packed)
246
296
 
247
297
  # bool #
248
298
  def read_bool(self, address: int) -> bool:
@@ -252,6 +302,9 @@ class EvasiveProcess:
252
302
  return bool(int.from_bytes(bool_byte, 'little'))
253
303
  except OSError:
254
304
  return False
305
+
306
+ def write_bool(self, address: int, value: bool) -> None:
307
+ self.write(address, (1 if value else 0).to_bytes(1, 'little'))
255
308
 
256
309
  # string #
257
310
  def read_raw_string(self, address: int, max_length: int = 256) -> str:
@@ -259,6 +312,12 @@ class EvasiveProcess:
259
312
  null_pos = buffer.find(b'\x00')
260
313
  valid_bytes = buffer[:null_pos] if null_pos != -1 else buffer
261
314
  return valid_bytes.decode('utf-8', errors='ignore')
315
+
316
+ def write_raw_string(self, address: int, value: str, null_terminate: bool = True) -> None:
317
+ data = value.encode('utf-8')
318
+ if null_terminate:
319
+ data += b'\x00'
320
+ self.write(address, data)
262
321
 
263
322
  def read_string(self, address: int) -> str:
264
323
  string_length = self.read_int(address + 0x10)
@@ -272,4 +331,4 @@ class EvasiveProcess:
272
331
  def close(self):
273
332
  if self.handle and self.handle.value != 0:
274
333
  nt_close_syscall(self.handle)
275
- self.handle = HANDLE(0)
334
+ self.handle = HANDLE(0)
@@ -32,8 +32,8 @@ except Exception:
32
32
  # Fallback defaults
33
33
  LoadedOffsets = {
34
34
  "Text": "0xC10",
35
- "Character": "0x328",
36
- "PrimaryPart": "0x268",
35
+ "Character": "0x340",
36
+ "PrimaryPart": "0x260",
37
37
  }
38
38
 
39
39
  ParseOffsets(LoadedOffsets, OffsetsRequest.json())
@@ -15,17 +15,105 @@ class RBXInstance:
15
15
  return self.FindFirstChild(key)
16
16
 
17
17
  # utilities #
18
+ def _ensure_writable(self):
19
+ if not hasattr(self.memory_module, "write"):
20
+ raise RuntimeError("Write operations require a memory module with write support (allow_write=True).")
21
+
22
+ @staticmethod
23
+ def _as_vector3(value, context="value"):
24
+ if isinstance(value, Vector3):
25
+ return value
26
+
27
+ if isinstance(value, (tuple, list)) and len(value) == 3:
28
+ return Vector3(*value)
29
+
30
+ raise TypeError(f"{context} must be a Vector3 or an iterable of three numbers.")
31
+
32
+ @staticmethod
33
+ def _as_vector2(value, context="value"):
34
+ if isinstance(value, Vector2):
35
+ return value
36
+
37
+ if isinstance(value, (tuple, list)) and len(value) == 2:
38
+ return Vector2(*value)
39
+
40
+ raise TypeError(f"{context} must be a Vector2 or an iterable of two numbers.")
41
+
42
+ @staticmethod
43
+ def _as_udim2(value, context="value"):
44
+ if isinstance(value, UDim2):
45
+ return value
46
+ if isinstance(value, (tuple, list)):
47
+ if len(value) == 4:
48
+ return UDim2(value[0], value[1], value[2], value[3])
49
+ if len(value) == 2:
50
+ x, y = value
51
+ if isinstance(x, UDim) and isinstance(y, UDim):
52
+ return UDim2(x.Scale, x.Offset, y.Scale, y.Offset)
53
+ if isinstance(x, (tuple, list)) and isinstance(y, (tuple, list)) and len(x) == 2 and len(y) == 2:
54
+ return UDim2(x[0], x[1], y[0], y[1])
55
+ raise TypeError(f"{context} must be a UDim2 or a compatible iterable.")
56
+
57
+ # memory helpers #
58
+ def _write_rbx_string(self, address: int, value: str):
59
+ self._ensure_writable()
60
+
61
+ if not isinstance(value, str):
62
+ raise TypeError("value must be a string.")
63
+
64
+ if address == 0:
65
+ raise ValueError("String address is null; cannot write.")
66
+
67
+ encoded = value.encode('utf-8')
68
+ if len(encoded) > 15:
69
+ raise ValueError("String too long (max 15 UTF-8 bytes supported for inline Roblox strings).")
70
+
71
+ current_length = self.memory_module.read_int(address + 0x10)
72
+ if current_length > 15:
73
+ raise ValueError("Existing string is heap allocated; inline overwrite is not supported.")
74
+
75
+ padded = encoded + b'\x00'
76
+ if len(padded) < 16:
77
+ padded += b'\x00' * (16 - len(padded))
78
+
79
+ self.memory_module.write(address, padded[:16])
80
+ self.memory_module.write_int(address + 0x10, len(encoded))
81
+
82
+
83
+ def _read_udim2(self, address: int) -> UDim2:
84
+ if not isinstance(address, int):
85
+ raise TypeError("address must be an int.")
86
+
87
+ scale_x = self.memory_module.read_float(address)
88
+ offset_x = self.memory_module.read_int(address + 0x4)
89
+ scale_y = self.memory_module.read_float(address + 0x8)
90
+ offset_y = self.memory_module.read_int(address + 0xC)
91
+
92
+ return UDim2(scale_x, offset_x, scale_y, offset_y)
93
+
94
+
95
+ def _write_udim2(self, address: int, value: UDim2):
96
+ value = self._as_udim2(value, "UDim2")
97
+
98
+ if not isinstance(value, UDim2):
99
+ raise TypeError("value must be a UDim2.")
100
+
101
+ self._ensure_writable()
102
+
103
+ self.memory_module.write_float(address, value.X.Scale)
104
+ self.memory_module.write_int(address + 0x4, value.X.Offset)
105
+
106
+ self.memory_module.write_float(address + 0x8, value.Y.Scale)
107
+ self.memory_module.write_int(address + 0xC, value.Y.Offset)
108
+
109
+
110
+ # useful pointer stuff #
18
111
  @property
19
112
  def primitive_address(self):
20
113
  part_primitive_pointer = self.raw_address + Offsets["Primitive"]
21
114
  part_primitive = int.from_bytes(self.memory_module.read(part_primitive_pointer, 8), 'little')
22
115
  return part_primitive
23
116
 
24
- @property
25
- def on_demand_instance_address(self):
26
- part_primitive_pointer = self.raw_address + Offsets["OnDemandInstance"]
27
- part_primitive = int.from_bytes(self.memory_module.read(part_primitive_pointer, 8), 'little')
28
- return part_primitive
29
117
 
30
118
  # props #
31
119
  @property
@@ -36,12 +124,32 @@ class RBXInstance:
36
124
 
37
125
  return RBXInstance(parent_pointer, self.memory_module)
38
126
 
127
+ @Parent.setter
128
+ def Parent(self, value):
129
+ if value is None:
130
+ target = 0
131
+ elif isinstance(value, RBXInstance):
132
+ target = value.raw_address
133
+ elif isinstance(value, int):
134
+ target = value
135
+ else:
136
+ raise TypeError("Parent must be set to an RBXInstance, int address, or None.")
137
+ self._ensure_writable()
138
+ self.memory_module.write_long(self.raw_address + Offsets["Parent"], target)
139
+
39
140
  @property
40
141
  def Name(self):
41
142
  name_address_pointer = self.raw_address + Offsets["Name"]
42
143
  name_address = int.from_bytes(self.memory_module.read(name_address_pointer, 8), 'little')
43
144
  return self.memory_module.read_string(name_address)
44
145
 
146
+ @Name.setter
147
+ def Name(self, value: str):
148
+ self._ensure_writable()
149
+ name_address_pointer = self.raw_address + Offsets["Name"]
150
+ name_address = int.from_bytes(self.memory_module.read(name_address_pointer, 8), 'little')
151
+ self._write_rbx_string(name_address, value)
152
+
45
153
  @property
46
154
  def ClassName(self):
47
155
  class_descriptor_address = int.from_bytes(
@@ -79,6 +187,29 @@ class RBXInstance:
79
187
  Vector3(*LookVectorData)
80
188
  )
81
189
 
190
+ @CFrame.setter
191
+ def CFrame(self, value: "CFrame"):
192
+ if not isinstance(value, CFrame):
193
+ raise TypeError("CFrame setter expects a CFrame value.")
194
+ self._ensure_writable()
195
+
196
+ matrix_data = [
197
+ value.RightVector.X, value.UpVector.X, -value.LookVector.X,
198
+ value.RightVector.Y, value.UpVector.Y, -value.LookVector.Y,
199
+ value.RightVector.Z, value.UpVector.Z, -value.LookVector.Z,
200
+ value.Position.X, value.Position.Y, value.Position.Z
201
+ ]
202
+
203
+ className = self.ClassName
204
+ if "part" in className.lower():
205
+ base_address = self.primitive_address + Offsets["CFrame"]
206
+ elif className == "Camera":
207
+ base_address = self.raw_address + Offsets["CameraCFrame"]
208
+ else:
209
+ raise AttributeError("CFrame cannot be written for this instance type.")
210
+
211
+ self.memory_module.write_floats(base_address, matrix_data)
212
+
82
213
  @property
83
214
  def Position(self):
84
215
  className = self.ClassName
@@ -89,17 +220,30 @@ class RBXInstance:
89
220
  position_vector3 = self.memory_module.read_floats(self.raw_address + Offsets["CameraPos"], 3)
90
221
  return Vector3(*position_vector3)
91
222
  else:
92
- try:
93
- x = self.memory_module.read_float(self.raw_address + Offsets["FramePositionX"])
94
- x_offset = self.memory_module.read_int(self.raw_address + Offsets["FramePositionOffsetX"])
223
+ return self._read_udim2(self.raw_address + Offsets["FramePositionX"])
224
+
225
+ @Position.setter
226
+ def Position(self, value):
227
+ className = self.ClassName
228
+
229
+ self._ensure_writable()
230
+ if "part" in className.lower():
231
+ vec = self._as_vector3(value, "Position")
232
+ self.memory_module.write_floats(
233
+ self.primitive_address + Offsets["Position"],
234
+ (vec.X, vec.Y, vec.Z)
235
+ )
95
236
 
96
- y = self.memory_module.read_float(self.raw_address + Offsets["FramePositionY"])
97
- y_offset = self.memory_module.read_int(self.raw_address + Offsets["FramePositionOffsetY"])
237
+ elif className == "Camera":
238
+ vec = self._as_vector3(value, "Position")
239
+ self.memory_module.write_floats(
240
+ self.raw_address + Offsets["CameraPos"],
241
+ (vec.X, vec.Y, vec.Z)
242
+ )
98
243
 
99
- return UDim2(x, x_offset, y, y_offset)
100
- except (KeyError, OSError) as e:
101
- print(f"Error reading position: {e}")
102
- return (0.0, 0, 0.0, 0)
244
+ else:
245
+ udim2_value = self._as_udim2(value, "Position")
246
+ self._write_udim2(self.raw_address + Offsets["FramePositionX"], udim2_value)
103
247
 
104
248
  @property
105
249
  def Velocity(self):
@@ -111,19 +255,40 @@ class RBXInstance:
111
255
 
112
256
  return None
113
257
 
258
+ @Velocity.setter
259
+ def Velocity(self, value):
260
+ className = self.ClassName
261
+ if "part" not in className.lower():
262
+ raise AttributeError("Velocity can only be written for BasePart-derived instances.")
263
+
264
+ vec = self._as_vector3(value, "Velocity")
265
+
266
+ self._ensure_writable()
267
+ self.memory_module.write_floats(
268
+ self.primitive_address + Offsets["Velocity"],
269
+ (vec.X, vec.Y, vec.Z)
270
+ )
271
+
114
272
  @property
115
273
  def Size(self):
116
274
  if "part" in self.ClassName.lower():
117
275
  size_vector3 = self.memory_module.read_floats(self.primitive_address + Offsets["PartSize"], 3)
118
276
  return Vector3(*size_vector3)
119
277
  else:
120
- try:
121
- x = self.memory_module.read_float(self.raw_address + Offsets["FrameSizeX"])
122
- y = self.memory_module.read_float(self.raw_address + Offsets["FrameSizeY"])
123
- return (x, y)
124
- except (KeyError, OSError) as e:
125
- print(f"Error reading position: {e}")
126
- return (0.0, 0.0)
278
+ return self._read_udim2(self.raw_address + Offsets["FrameSizeX"])
279
+
280
+ @Size.setter
281
+ def Size(self, value):
282
+ self._ensure_writable()
283
+ if "part" in self.ClassName.lower():
284
+ vec = self._as_vector3(value, "Size")
285
+ self.memory_module.write_floats(
286
+ self.primitive_address + Offsets["PartSize"],
287
+ (vec.X, vec.Y, vec.Z)
288
+ )
289
+ else:
290
+ gui_size = self._as_udim2(value, "Size")
291
+ self._write_udim2(self.raw_address + Offsets["FrameSizeX"], gui_size)
127
292
 
128
293
  # XXXXValue props #
129
294
  @property
@@ -148,6 +313,33 @@ class RBXInstance:
148
313
  return RBXInstance(object_address, self.memory_module)
149
314
 
150
315
  return None
316
+
317
+ @Value.setter
318
+ def Value(self, new_value):
319
+ self._ensure_writable()
320
+ classname = self.ClassName
321
+ value_address = self.raw_address + Offsets["Value"]
322
+
323
+ if classname == "StringValue":
324
+ self._write_rbx_string(value_address, str(new_value))
325
+ elif classname == "IntValue":
326
+ self.memory_module.write_int(value_address, int(new_value))
327
+ elif classname == "NumberValue":
328
+ self.memory_module.write_double(value_address, float(new_value))
329
+ elif classname == "BoolValue":
330
+ self.memory_module.write_bool(value_address, bool(new_value))
331
+ elif classname == "ObjectValue":
332
+ if new_value is None:
333
+ target = 0
334
+ elif isinstance(new_value, RBXInstance):
335
+ target = new_value.raw_address
336
+ elif isinstance(new_value, int):
337
+ target = new_value
338
+ else:
339
+ raise TypeError("ObjectValue.Value must be set to an RBXInstance, int address, or None.")
340
+ self.memory_module.write_long(value_address, target)
341
+ else:
342
+ raise AttributeError(f"Writing Value is not supported for class {classname}.")
151
343
 
152
344
  # text props #
153
345
  @property
@@ -157,6 +349,12 @@ class RBXInstance:
157
349
 
158
350
  return None
159
351
 
352
+ @Text.setter
353
+ def Text(self, value: str):
354
+ if "text" not in self.ClassName.lower():
355
+ raise AttributeError("Text is not available on this instance.")
356
+ self._write_rbx_string(self.raw_address + Offsets["Text"], str(value))
357
+
160
358
  # humanoid props #
161
359
  @property
162
360
  def WalkSpeed(self):
@@ -165,12 +363,28 @@ class RBXInstance:
165
363
 
166
364
  return self.memory_module.read_float(self.raw_address + Offsets["WalkSpeed"])
167
365
 
366
+ @WalkSpeed.setter
367
+ def WalkSpeed(self, value: float):
368
+ if self.ClassName != "Humanoid":
369
+ raise AttributeError("WalkSpeed is only available on Humanoid instances.")
370
+ self._ensure_writable()
371
+
372
+ self.memory_module.write_float(self.raw_address + Offsets["WalkSpeed"], float(value))
373
+
168
374
  @property
169
375
  def JumpPower(self):
170
376
  if self.ClassName != "Humanoid":
171
377
  return None
172
378
 
173
379
  return self.memory_module.read_float(self.raw_address + Offsets["JumpPower"])
380
+
381
+ @JumpPower.setter
382
+ def JumpPower(self, value: float):
383
+ if self.ClassName != "Humanoid":
384
+ raise AttributeError("JumpPower is only available on Humanoid instances.")
385
+ self._ensure_writable()
386
+
387
+ self.memory_module.write_float(self.raw_address + Offsets["JumpPower"], float(value))
174
388
 
175
389
  @property
176
390
  def Health(self):
@@ -179,6 +393,14 @@ class RBXInstance:
179
393
 
180
394
  return self.memory_module.read_float(self.raw_address + Offsets["Health"])
181
395
 
396
+ @Health.setter
397
+ def Health(self, value: float):
398
+ if self.ClassName != "Humanoid":
399
+ raise AttributeError("Health is only available on Humanoid instances.")
400
+ self._ensure_writable()
401
+
402
+ self.memory_module.write_float(self.raw_address + Offsets["Health"], float(value))
403
+
182
404
  @property
183
405
  def MaxHealth(self):
184
406
  if self.ClassName != "Humanoid":
@@ -186,6 +408,14 @@ class RBXInstance:
186
408
 
187
409
  return self.memory_module.read_float(self.raw_address + Offsets["MaxHealth"])
188
410
 
411
+ @MaxHealth.setter
412
+ def MaxHealth(self, value: float):
413
+ if self.ClassName != "Humanoid":
414
+ raise AttributeError("MaxHealth is only available on Humanoid instances.")
415
+ self._ensure_writable()
416
+
417
+ self.memory_module.write_float(self.raw_address + Offsets["MaxHealth"], float(value))
418
+
189
419
  # model props #
190
420
  @property
191
421
  def PrimaryPart(self):
@@ -197,6 +427,22 @@ class RBXInstance:
197
427
  return None
198
428
 
199
429
  return RBXInstance(parent_pointer, self.memory_module)
430
+
431
+ @PrimaryPart.setter
432
+ def PrimaryPart(self, value):
433
+ if self.ClassName != "Model":
434
+ raise AttributeError("PrimaryPart is only available on Model instances.")
435
+ self._ensure_writable()
436
+
437
+ if value is None:
438
+ target = 0
439
+ elif isinstance(value, RBXInstance):
440
+ target = value.raw_address
441
+ elif isinstance(value, int):
442
+ target = value
443
+ else:
444
+ raise TypeError("PrimaryPart must be set to an RBXInstance, int address, or None.")
445
+ self.memory_module.write_long(self.raw_address + Offsets["PrimaryPart"], target)
200
446
 
201
447
  # functions #
202
448
  def GetChildren(self):
@@ -301,6 +547,10 @@ class PlayerClass(RBXInstance):
301
547
  def DisplayName(self):
302
548
  return self.memory_module.read_string(self.raw_address + Offsets["DisplayName"])
303
549
 
550
+ @DisplayName.setter
551
+ def DisplayName(self, value: str):
552
+ self._write_rbx_string(self.raw_address + Offsets["DisplayName"], str(value))
553
+
304
554
  @property
305
555
  def UserId(self):
306
556
  return self.memory_module.read_long(self.raw_address + Offsets["UserId"])
@@ -330,16 +580,34 @@ class CameraClass(RBXInstance):
330
580
  @property
331
581
  def FieldOfView(self):
332
582
  return self.FieldOfViewRadians * (180/math.pi)
583
+
584
+ @FieldOfView.setter
585
+ def FieldOfView(self, value: float):
586
+ self.FieldOfViewRadians = float(value) * (math.pi / 180)
333
587
 
334
588
  @property
335
589
  def FieldOfViewRadians(self):
336
590
  return self.memory_module.read_float(self.raw_address + Offsets["FOV"])
591
+
592
+ @FieldOfViewRadians.setter
593
+ def FieldOfViewRadians(self, value: float):
594
+ self._ensure_writable()
595
+ self.memory_module.write_float(self.raw_address + Offsets["FOV"], float(value))
337
596
 
338
597
  @property
339
598
  def ViewportSize(self):
340
599
  SizeData = self.memory_module.read_floats(self.raw_address + Offsets["ViewportSize"], 2)
341
600
  return Vector2(*SizeData)
342
601
 
602
+ @ViewportSize.setter
603
+ def ViewportSize(self, value):
604
+ vec = self._as_vector2(value, "ViewportSize")
605
+ self._ensure_writable()
606
+ self.memory_module.write_floats(
607
+ self.raw_address + Offsets["ViewportSize"],
608
+ (vec.X, vec.Y)
609
+ )
610
+
343
611
  # Service #
344
612
  class ServiceBase:
345
613
  def __init__(self):
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robloxmemoryapi
3
- Version: 0.0.2
4
- Summary: Python Library that abstracts reading data from the Roblox DataModel
3
+ Version: 0.0.4
4
+ Summary: Python Library that abstracts reading and writing data from the Roblox DataModel
5
5
  Author-email: upio <notpoiu@users.noreply.github.com>, mstudio45 <mstudio45@users.noreply.github.com>, ActualMasterOogway <ActualMasterOogway@users.noreply.github.com>
6
6
  License: Copyright 2025 upio, mstudio45, master oogway
7
7
 
@@ -58,12 +58,26 @@ from robloxmemoryapi import RobloxGameClient
58
58
  client = RobloxGameClient()
59
59
  ```
60
60
 
61
+ To request write access (e.g. to patch values), pass `allow_write=True` when creating the client.
62
+
63
+ ```python
64
+ client = RobloxGameClient(allow_write=True)
65
+ ```
66
+
61
67
  Access the data model:
62
68
 
63
69
  ```python
64
70
  game = client.DataModel
65
71
  ```
66
72
 
73
+ Kill the local player:
74
+
75
+ ```python
76
+ # (requires allow_write=True when creating the client)
77
+ # allow_write may be detected by roblox. It is disabled by default.
78
+ game.Players.LocalPlayer.Character.Humanoid.Health = 0
79
+ ```
80
+
67
81
  Get the local player's name:
68
82
 
69
83
  ```python
@@ -1,6 +0,0 @@
1
- {
2
- "Text": "0xC10",
3
- "Character": "0x328",
4
- "PrimaryPart": "0x268"
5
- }
6
-