World External · Luau VM

Your First Script

Resolving the DataModel, walking to a player, and reading and writing something real.

Start from the DataModel

Every script starts the same way: get the DataModel address, then walk down from it. Nothing is global and nothing is cached for you.

first.lua
local dm = get_datamodel()
print("datamodel at", dm)

local players = find_first_child(dm, "Players")
local workspace_ = find_first_child(dm, "Workspace")

print(get_classname(players))     --> "Players"
print(get_instance_path(players)) --> "Players"

Walking the tree

get_children returns a table of addresses. Everything else — names, classes, parents — is a call rather than a field.

Listing players
local dm = get_datamodel()
local players = find_first_child(dm, "Players")

for _, player in ipairs(get_children(players)) do
    print(read_name(player), get_classname(player))

    local character = find_first_child(player, "Character")
    if character ~= 0 then
        local root = find_first_child(character, "HumanoidRootPart")
        if root ~= 0 then
            print("  at", read_BasePart_Position and "..." or root)
        end
    end
end

Zero means nothing found

The explorer functions return 0, not nil, when they find nothing. Check ~= 0 before you use an address — passing 0 to a read accessor is how you get a garbage value instead of an error.

Reading and writing

Two ways to get at a value. The offset accessors are the readable option and know the type for you; the raw memory functions are there when you have an address and know what is at it.

Both routes
local part = find_instance_by_path("Workspace.Baseplate")

-- Named accessor: knows the offset and the type.
print(read_BasePart_Transparency(part))
write_BasePart_Transparency(part, 0.5)

-- Raw: you supply the address and pick the width.
local address = part + 0x100
print(read_float(address))
write_float(address, 1.0)

Raw writes are not checked

A write to the wrong address does not error — it corrupts whatever was there. Read the value back first and satisfy yourself it looks like what you expect before you write.

Doing it every frame

There are no events. Anything continuous is a loop with a wait, and task.spawn puts it on its own thread so it does not block the rest of your script.

A polling loop
task.spawn(function()
    while true do
        local dm = get_datamodel()
        if dm ~= 0 then
            local players = find_first_child(dm, "Players")
            for _, player in ipairs(get_children(players)) do
                -- per-player work
            end
        end
        wait(0.1)
    end
end)

Resolve from get_datamodel() inside the loop rather than above it. That way a rejoin picks up the new tree instead of leaving the loop reading a dead one.