Guide: Working With Memory
Resolving addresses that stay valid, following pointer chains, and not corrupting the game while you do it.
The single rule that prevents most External bugs: an address is only valid for as long as the thing it points at lives. Rejoin and every address you hold is stale. Destroy an instance and its address is reused by something else.
-- Wrong: resolved once, used forever.
local part = find_instance_by_path("Workspace.Baseplate")
task.spawn(function()
while true do
write_BasePart_Transparency(part, 0.5) -- garbage after a rejoin
wait(0.1)
end
end)
-- Right: resolved inside the loop.
task.spawn(function()
while true do
local part = find_instance_by_path("Workspace.Baseplate")
if part ~= 0 then
write_BasePart_Transparency(part, 0.5)
end
wait(0.1)
end
end)Resolve per pass, cache within one
Resolving every pass is the safe default. Caching *within* a single pass — resolving Players once and reusing it for every player in that iteration — is free and correct.
Anything interesting is usually two or three hops away. Read a pointer with read_qword, then read from what it points at. Every hop can fail, so every hop needs a check.
local function readChain(base, ...)
local address = base
for _, offset in ipairs({...}) do
if address == 0 then return 0 end
address = read_qword(address + offset)
end
return address
end
local part = find_instance_by_path("Workspace.Baseplate")
local primitive = readChain(part, 0x160)
if primitive ~= 0 then
print(read_float(primitive + 0x10))
end- Read before you write. If the current value does not look like what you expect, you have the wrong address — stop.
- Match the width. Writing a float where a qword lives clobbers half a pointer, and the crash lands somewhere unrelated.
- Prefer named accessors. They know the offset and the type. Raw arithmetic is for things the accessors do not cover.
- Change one thing at a time. When something breaks, you want to know which write did it.
- Never write to address 0. Guard every result — the explorer functions return 0, not nil.
local function safeWrite(address, expected, value)
if address == 0 then return false end
if math.abs(read_float(address) - expected) > 0.001 then
warn("unexpected value at " .. address .. ", refusing to write")
return false
end
return write_float(address, value)
endEvery read is a cross-process operation. A loop that reads a thousand values per frame will be noticeably slower than one that reads fifty.
- Resolve the container once per pass, not once per item.
- Use find_first_child rather than get_children plus a scan when you know the name.
- Do not run at 60Hz what is fine at 10Hz — pick the interval per job.
- Read a pointer once and reuse it for every property behind it, within the same pass.