World External · Luau VM

Guide: Working With Memory

Resolving addresses that stay valid, following pointer chains, and not corrupting the game while you do it.

Addresses have lifetimes

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.

Cache nothing across a wait
-- 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.

Pointer chains

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.

A guarded chain
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

Not breaking the game

  • 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.
A defensive write
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)
end

Keeping it fast

Every 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.