Skip to content

Commit 500c709

Browse files
committed
feat: add AnimationTrack class, Animator.GetPlayingAnimationTracks, and Humanoid.MoveTo
- Added AnimationTrack datastructure with Animation, Animator, Speed, Looped, IsPlaying properties - Added GetPlayingAnimationTracks() for Animator instances (linked list traversal with validation) - Added MoveTo(target, wait) for Humanoid instances (supports Vector3 and Part targets, sync/async) - Bumped version to 0.2.0
1 parent b225e1a commit 500c709

4 files changed

Lines changed: 139 additions & 9 deletions

File tree

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "robloxmemoryapi"
7-
version = "0.1.9.2"
7+
version = "0.2.0"
88
description = "Python Library that abstracts reading and writing data from the Roblox DataModel"
99
readme = { file = "README.md", content-type = "text/markdown" }
1010
requires-python = ">=3.9"

‎src/robloxmemoryapi/utils/memory.py‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,20 @@ def __init__(self, pid: int, access: DWORD):
211211
if status != STATUS_SUCCESS:
212212
raise ctypes.WinError(f"NtOpenProcess failed with NTSTATUS: 0x{status:X}")
213213
self.base = _get_module_base(self.handle)
214+
self.is_closed = False
214215
if self.base == 0:
215216
self.close()
216217
raise ConnectionError("Failed to get module base address.")
217218

219+
@property
220+
def is_invalid_handle(self) -> bool:
221+
return self.handle and self.handle.value == 0
222+
218223
def read(self, address: int, size: int) -> bytes:
219-
if not self.handle or self.handle.value == 0:
224+
if self.is_closed:
225+
return b""
226+
227+
if self.is_invalid_handle:
220228
raise ValueError("Process handle is not valid.")
221229
buffer = ctypes.create_string_buffer(size)
222230
bytes_read = ctypes.c_ulong(0)
@@ -232,7 +240,7 @@ def read(self, address: int, size: int) -> bytes:
232240
return buffer.raw[:bytes_read.value]
233241

234242
def write(self, address: int, data: bytes | bytearray) -> int:
235-
if not self.handle or self.handle.value == 0:
243+
if self.is_invalid_handle:
236244
raise ValueError("Process handle is not valid.")
237245
if not isinstance(data, (bytes, bytearray)):
238246
raise TypeError("data must be bytes-like.")
@@ -256,7 +264,10 @@ def virtual_alloc(self, size: int, allocation_type: int = MEM_COMMIT | MEM_RESER
256264
if size <= 0:
257265
raise ValueError("size must be greater than zero.")
258266

259-
if not self.handle or self.handle.value == 0:
267+
if self.is_closed:
268+
return
269+
270+
if self.is_invalid_handle:
260271
raise ValueError("Process handle is not valid.")
261272

262273
base_address = LPVOID()
@@ -417,6 +428,7 @@ def get_address(self, address: int, pointer: bool) -> int:
417428

418429
#########
419430
def close(self):
420-
if self.handle and self.handle.value != 0:
431+
if not self.is_invalid_handle:
432+
self.is_closed = True
421433
nt_close_syscall(self.handle)
422434
self.handle = HANDLE(0)

‎src/robloxmemoryapi/utils/rbx/datastructures.py‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,4 +353,40 @@ def GetComponents(self):
353353
self.RightVector.Y, self.UpVector.Y, self.LookVector.Y,
354354
self.RightVector.Z, self.UpVector.Z, self.LookVector.Z,
355355
self.Position.X, self.Position.Y, self.Position.Z
356-
])
356+
])
357+
358+
class AnimationTrack:
359+
def __init__(self, address, memory_module, offsets):
360+
self.raw_address = address
361+
self.memory_module = memory_module
362+
self._offsets = offsets
363+
364+
def __repr__(self):
365+
return f"AnimationTrack(0x{self.raw_address:X})"
366+
367+
def __eq__(self, other):
368+
return isinstance(other, AnimationTrack) and self.raw_address == other.raw_address
369+
370+
@property
371+
def Animation(self):
372+
from .instance import RBXInstance
373+
ptr = self.memory_module.get_pointer(self.raw_address, self._offsets["Animation"])
374+
return RBXInstance(ptr, self.memory_module) if ptr != 0 else None
375+
376+
@property
377+
def Animator(self):
378+
from .instance import RBXInstance
379+
ptr = self.memory_module.get_pointer(self.raw_address, self._offsets["Animator"])
380+
return RBXInstance(ptr, self.memory_module) if ptr != 0 else None
381+
382+
@property
383+
def Speed(self):
384+
return self.memory_module.read_float(self.raw_address, self._offsets["Speed"])
385+
386+
@property
387+
def Looped(self):
388+
return self.memory_module.read_bool(self.raw_address, self._offsets["Looped"])
389+
390+
@property
391+
def IsPlaying(self):
392+
return self.memory_module.read_bool(self.raw_address, self._offsets["IsPlaying"])

‎src/robloxmemoryapi/utils/rbx/instance.py‎

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
clickdetector_offsets = Offsets["ClickDetector"]
1717
statsitem_offsets = Offsets["StatsItem"]
1818
inputobject_offsets = Offsets["MouseService"] # InputObject uses MouseService offsets
19+
animator_offsets = Offsets["Animator"]
20+
animationtrack_offsets = Offsets["AnimationTrack"]
1921

2022
ROTATION_MATRIX_FLOATS = 9
2123

@@ -115,7 +117,6 @@ def primitive_address(self):
115117
self.raw_address,
116118
basepart_offsets["Primitive"]
117119
)
118-
119120

120121
# props #
121122
@property
@@ -173,7 +174,7 @@ def ClassName(self):
173174
instance_offsets["ClassName"]
174175
)
175176
return self.memory_module.read_string(class_name_address)
176-
177+
177178
@property
178179
def CFrame(self):
179180
className = self.ClassName
@@ -557,6 +558,36 @@ def CanTouch(self, value: bool):
557558
flags &= ~Offsets["PrimitiveFlags"]["CanTouch"]
558559
self._write_primitive_flags(flags)
559560

561+
# Animator props #
562+
def GetPlayingAnimationTracks(self):
563+
if self.ClassName != "Animator":
564+
raise AttributeError("GetPlayingAnimationTracks is only available on Animator instances.")
565+
566+
head = self.memory_module.get_pointer(
567+
self.raw_address,
568+
animator_offsets["ActiveAnimations"]
569+
)
570+
if head == 0:
571+
return []
572+
573+
node = self.memory_module.get_pointer(head)
574+
result = []
575+
576+
while node != 0 and node != head:
577+
track_address = self.memory_module.get_pointer(node, 0x10)
578+
animation_track = AnimationTrack(track_address, self.memory_module, animationtrack_offsets)
579+
580+
try:
581+
if animation_track.Animation.AnimationId is None:
582+
raise Exception("AnimationId is None")
583+
584+
result.append(animation_track)
585+
except Exception:
586+
pass
587+
node = self.memory_module.get_pointer(node)
588+
589+
return result
590+
560591
# ProximityPrompt props #
561592
@property
562593
def MaxActivationDistance(self):
@@ -1137,6 +1168,58 @@ def HumanoidState(self, value: int):
11371168
int(value)
11381169
)
11391170

1171+
def MoveTo(self, target, wait=True):
1172+
if self.ClassName != "Humanoid":
1173+
raise AttributeError("MoveTo is only available on Humanoid instances.")
1174+
1175+
self._ensure_writable()
1176+
1177+
if isinstance(target, RBXInstance):
1178+
self.memory_module.write_long(
1179+
self.raw_address + humanoid_offsets["MoveToPart"],
1180+
target.raw_address
1181+
)
1182+
position = target.Position
1183+
else:
1184+
position = self._as_vector3(target, "MoveTo target")
1185+
1186+
character = self.Parent
1187+
if character is None:
1188+
raise RuntimeError("Humanoid has no parent Character model.")
1189+
1190+
hrp = character.PrimaryPart
1191+
if hrp is None:
1192+
raise RuntimeError("Could not find PrimaryPart in Character.")
1193+
1194+
def execute_move():
1195+
while True:
1196+
try:
1197+
if self.memory_module.is_invalid_handle or self.memory_module.is_closed:
1198+
break
1199+
1200+
if hrp.Parent is None or self.Health <= 0:
1201+
break
1202+
1203+
current = hrp.Position
1204+
if abs(current.X - position.X) <= 1.0 and abs(current.Z - position.Z) <= 1.0:
1205+
break
1206+
1207+
self.memory_module.write_floats(
1208+
self.raw_address + humanoid_offsets["MoveToPoint"],
1209+
(position.X, position.Y, position.Z)
1210+
)
1211+
self.memory_module.write_bool(
1212+
self.raw_address + humanoid_offsets["IsWalking"],
1213+
True
1214+
)
1215+
except Exception:
1216+
break
1217+
1218+
if wait:
1219+
execute_move()
1220+
else:
1221+
threading.Thread(target=execute_move, daemon=True).start()
1222+
11401223
# adornee / animation props #
11411224
@property
11421225
def Adornee(self):
@@ -1183,7 +1266,6 @@ def AnimationId(self, value: str):
11831266
str(value)
11841267
)
11851268

1186-
11871269
# model props #
11881270
@property
11891271
def PrimaryPart(self):

0 commit comments

Comments
 (0)