roblox-studio ยท diff
git:20260718.c3e8197 to git:20260726.4086893
46 added, 52 removed. Audit A to A.
---
name: roblox-studio
description: Build and edit in Roblox Studio via the Roblox Studio MCP the RIGHT way - preflight the Studio connection, do the whole build in a few big execute_luau scripts (not dozens of tiny calls), make REALISTIC terrain with the Terrain VOXEL api driven by Perlin noise (NEVER stacked Parts or concentric layers - those give ugly blocky stepped pyramids), poll generative jobs, check the console, and fail honestly. Invoke for ANY "in Roblox / Roblox Studio" request - terrain, mountains, parts, scripts, models, materials, assets.
metadata:
openclaw:
emoji: "๐ฎ"
tlamatini:
runtime: in-process
requires_tools: []
requires_mcps: []
budget:
max_iterations: 64
max_seconds: 1800
max_tokens: 120000
permissions:
filesystem: { read: [], write: [] }
shell: []
network: deny
db: deny
inputs:
- { name: objective, type: string, required: true, description: "What to build or edit in Roblox Studio." }
outputs:
- { name: summary, type: string, required: true, description: "What was built and how to verify it in Studio." }
triggers:
keywords: ["roblox","roblox studio","luau","lua script","terrain","mountain","voxel","fillball","fillblock","writevoxels","generate mesh","procedural model","generate material","baseplate","insert asset","studio"]
---
<!--
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โฆ T L A M A T I N I โฆ โ "one who knows"
Created by Angela Lรณpez Mendoza ยท @angelahack1
Tlamatini Author Banner โ do not remove (Angela's name is kept in every build)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-->
# Roblox Studio โ build it right, build it once, make it look REAL
- Follow this runbook whenever the user wants something built or edited in **Roblox Studio**. The Roblox tools are the External-MCP tools named `ext__Roblox_Studio__<tool>` (require Multi-Turn + ACPX on). Work as an OPERATOR: preflight, do the work in a FEW big scripts, verify, report โ never spray dozens of tiny calls or loop on a failing one.
+ Runbook for anything built or edited in **Roblox Studio**. The tools are the External-MCP tools `ext__Roblox_Studio__<tool>` (need Multi-Turn + ACPX on). Work as an OPERATOR: preflight, build in a FEW big scripts, verify, report.
- ## STEP 0 โ Preflight the connection (ALWAYS, before any build)
+ ## STEP 0 โ Preflight (ALWAYS, before any build)
- 1. `external_mcp_status` โ confirm `Roblox_Studio` is connected. If not: `external_mcp_reconnect` then `external_mcp_wait` (a first-run stdio child can take a few seconds).
- 2. `ext__Roblox_Studio__list_roblox_studios` โ if more than one, `ext__Roblox_Studio__set_active_studio` on the intended one.
- 3. `ext__Roblox_Studio__get_studio_state` โ confirm a place is open and the plugin is connected.
- 4. **If NO Studio is connected:** STOP and tell the user in one line: *"Open Roblox Studio with a place, make sure the MCP plugin is running, then say go."* Do not fake a build.
+ 1. `external_mcp_status` โ is `Roblox_Studio` connected? If not: `external_mcp_reconnect`, then `external_mcp_wait` (a first-run stdio child takes seconds).
+ 2. `list_roblox_studios` โ if more than one, `set_active_studio` on the intended one.
+ 3. `get_studio_state` โ a place must be open with the plugin connected.
+ 4. **No Studio connected:** STOP and say *"Open Roblox Studio with a place, make sure the MCP plugin is running, then say go."* Never fake a build.
- ## Tool map โ pick the RIGHT tool
+ ## Tool map
| Want toโฆ | Use |
|---|---|
- | Run ANY Luau (build terrain/parts/scripts, set properties) | `ext__Roblox_Studio__execute_luau` โ your workhorse |
- | Wait for an async job (generate_*) to finish | `ext__Roblox_Studio__wait_job_finished` |
- | See errors / prints from the last run | `ext__Roblox_Studio__get_console_output` |
- | Read the scene tree / find instances | `ext__Roblox_Studio__inspect_instance`, `search_game_tree` |
- | AI-generate an organic mesh / model / material | `generate_mesh`, `generate_procedural_model`, `generate_material` (async โ poll `wait_job_finished`) |
+ | Run ANY Luau (terrain, parts, scripts, properties) | `execute_luau` โ the workhorse |
+ | Wait for an async `generate_*` job | `wait_job_finished` |
+ | See errors / prints | `get_console_output` |
+ | Read the scene tree | `inspect_instance`, `search_game_tree` |
+ | AI-generate an organic mesh/model/material | `generate_mesh`, `generate_procedural_model`, `generate_material` (async โ poll) |
| Marketplace asset | `search_asset` โ `insert_asset` |
| Author/read scripts | `multi_edit`, `script_read`, `script_search`, `script_grep` |
- | See the result on screen | `ext__Roblox_Studio__screen_capture` |
+ | See the result | `screen_capture` |
- **Prefer `execute_luau` for deterministic geometry** (terrain, walls, layouts). Reserve `generate_mesh`/`generate_procedural_model` for organic one-off props the user explicitly wants AI-generated.
+ Use `execute_luau` for deterministic geometry; reserve the generative tools for organic one-off props.
## GOLDEN RULES
- 1. **Batch.** Do the ENTIRE build in ONE (or a few) `execute_luau` script(s) that loop โ not one part/mountain per call.
- 2. **REALISM = Terrain VOXELS + Perlin noise (see the terrain section โ this is the #1 thing people get wrong).** Never build landscape from `Part`s and never use symmetric concentric layers โ both look fake and blocky.
- 3. **Wrap every script in `pcall`** and finish with `print("TLM_OK <what happened>")` (else `warn(err)`), then read `get_console_output` to confirm.
- 4. **Correct Luau types.** `Vector3.new(x,y,z)` needs three NUMBERS; `Region3`/`CFrame` likewise. `"Unable to cast double to Vector3"` = you passed a number where a Vector3 was expected โ fix the call, don't retry it unchanged.
- 5. **Undo-friendly:** wrap edits in `ChangeHistoryService:TryBeginRecording(...)` / `:FinishRecording(...)`.
- 6. **Never loop on a failing tool.** If a call errors twice, STOP, read `get_console_output`, fix the root cause OR tell the user honestly. (The executor also blocks a call repeated 3ร.)
- 7. **Verify then report.** Never say "done" until `get_console_output` (and ideally `screen_capture`) confirms it.
+ 1. **Batch** โ the ENTIRE build in one or a few looping `execute_luau` scripts, not one part per call.
+ 2. **Realism = Terrain VOXELS + Perlin noise** (below). Never landscape out of `Part`s, never symmetric concentric layers.
+ 3. **Wrap every script in `pcall`**, end with `print("TLM_OK โฆ")` (else `warn(err)`), then confirm via `get_console_output`.
+ 4. **Correct Luau types** โ `Vector3.new(x,y,z)` takes three NUMBERS. `"Unable to cast double to Vector3"` means you passed a number where a Vector3 belongs: fix it, don't retry unchanged.
+ 5. **Undo-friendly** โ `ChangeHistoryService:TryBeginRecording(...)` / `:FinishRecording(...)`.
+ 6. **Never loop on a failing tool.** Two errors โ stop, read the console, fix the cause or say so honestly (the executor blocks a 3ร repeat anyway).
+ 7. **Verify, then report.** "Done" only after the console (ideally a capture) confirms it.
- ## TERRAIN & MOUNTAINS โ make them REALISTIC (this is where builds go WRONG)
+ ## TERRAIN & MOUNTAINS โ where builds go WRONG
- Realistic terrain has **two hard requirements**. Skip either and you get **ugly blocky STEPPED PYRAMIDS (a ziggurat)** โ that is a FAIL, not a mountain:
+ Two hard requirements; skip either and you get **blocky STEPPED PYRAMIDS** โ a FAIL, not a mountain:
- 1. **Use `workspace.Terrain` VOXELS โ NEVER `Part`s / `WedgePart`s.** Stacked Parts show hard rectangular STEPS. Terrain voxels smooth into continuous rock/snow.
- 2. **Drive the shape with PERLIN NOISE (`math.noise`) โ NEVER concentric symmetric layers.** Concentric shrinking disks/squares = a perfect cone or a stepped ziggurat. Real mountains are IRREGULAR: asymmetric peaks, ridges, spurs, foothills, no two slopes alike.
+ 1. **`workspace.Terrain` VOXELS, never `Part`s** โ stacked parts show hard rectangular steps; voxels smooth into rock/snow.
+ 2. **Shape driven by PERLIN NOISE (`math.noise`), never concentric layers** โ concentric shrinking disks give a cone or a ziggurat. Real mountains are irregular: asymmetric peaks, ridges, spurs, no two slopes alike.
- **The right way โ a Perlin-noise HEIGHTMAP written with `Terrain:WriteVoxels` (ONE call, smooth, natural).** For each (x,z): surface height = summed peak falloffs (smoothstep โ rounded base, not a sharp tip) **plus multi-octave `math.noise`**; then fill voxels below it โ Rock, Snow above a NOISY snowline, Grass at the base:
+ **The right way โ a Perlin-noise heightmap written with ONE `Terrain:WriteVoxels`.** Per (x,z): height = summed peak falloffs (smoothstep โ rounded base) **plus multi-octave `math.noise`**; fill below it โ Rock, Snow above a NOISY snowline, Grass at the base:
```lua
local Terrain = workspace.Terrain
- local RES = 4 -- voxel size in studs (4 = detailed, 8 = faster/coarser)
- local W = 512 -- terrain is W x W studs, centered on origin
- local peaks = { -- jitter these; DIFFERENT heights/spreads = natural
+ local RES = 4 -- voxel studs (4 = detailed, 8 = faster)
+ local W = 512 -- W x W studs, centered on origin
+ local peaks = { -- jitter these; DIFFERENT heights/spreads = natural
{x=0, z=0, h=150, r=175},
{x=-150,z=-130,h=95, r=120},
{x=165, z=140, h=120, r=135},
{x=-135,z=150, h=62, r=95 },
{x=170, z=-125,h=48, r=80 },
}
local AIR,ROCK,SNOW,GRASS = Enum.Material.Air,Enum.Material.Rock,Enum.Material.Snow,Enum.Material.Grass
local function surfaceY(wx, wz)
- local h = 6 -- flat-ish base ground
+ local h = 6 -- flat-ish base ground
for _,p in ipairs(peaks) do
local dx,dz = wx-p.x, wz-p.z
- local d = math.sqrt(dx*dx + dz*dz)
- local f = math.clamp(1 - d/p.r, 0, 1)
- f = f*f*(3 - 2*f) -- smoothstep => rounded, no cone tip
+ local f = math.clamp(1 - math.sqrt(dx*dx+dz*dz)/p.r, 0, 1)
+ f = f*f*(3 - 2*f) -- smoothstep => no cone tip
h = h + p.h*f
end
- -- multi-octave value noise: ridges + roughness + asymmetry (THIS is what makes it REAL)
+ -- multi-octave noise: ridges + roughness + asymmetry (THIS makes it REAL)
h = h + math.noise(wx*0.006, wz*0.006, 0.3)*40
+ math.noise(wx*0.015, wz*0.015, 2.7)*15
+ math.noise(wx*0.045, wz*0.045, 6.1)*5
return math.max(2, h)
end
local ok, err = pcall(function()
local region = Region3.new(Vector3.new(-W/2,0,-W/2), Vector3.new(W/2,176,W/2)):ExpandToGrid(RES)
local size = region.Size/RES
- local origin = region.CFrame.Position - region.Size/2 -- world min corner
+ local origin = region.CFrame.Position - region.Size/2 -- world min corner
local mats, occ = {}, {}
for x=1,size.X do mats[x]={} occ[x]={}
for y=1,size.Y do mats[x][y]={} occ[x][y]={}
for z=1,size.Z do
local wx = origin.X + (x-0.5)*RES
local wy = origin.Y + (y-0.5)*RES
local wz = origin.Z + (z-0.5)*RES
local s = surfaceY(wx, wz)
if wy <= s then
occ[x][y][z] = 1
- local snowline = 92 + math.noise(wx*0.02, wz*0.02, 4.0)*22 -- ragged snow edge
+ local snowline = 92 + math.noise(wx*0.02, wz*0.02, 4.0)*22 -- ragged edge
mats[x][y][z] = (wy > snowline and SNOW) or (wy < 9 and GRASS or ROCK)
else
occ[x][y][z] = 0; mats[x][y][z] = AIR
end
end
end
end
Terrain:WriteVoxels(region, RES, mats, occ)
end)
if ok then
- print(("TLM_OK realistic terrain: %d peaks, %dx%d studs, Perlin-noise ridged, snow-capped"):format(#peaks, W, W))
+ print(("TLM_OK terrain: %d peaks, %dx%d studs, noise-ridged, snow-capped"):format(#peaks, W, W))
else
warn("TLM_FAIL "..tostring(err))
end
```
- Tune to the user's spec: number/height/spread of `peaks`, `W` (terrain size), `RES` (detail vs speed โ bump to 8 if the volume is huge), the three `math.noise` amplitudes (bigger = rougher ridges). Keep peak centers inside `ยฑW/2`. **`Terrain:WriteVoxels` caps at ~4.19M voxels per call** โ for a bigger world, loop the region in โค~256-stud chunks. Only claim success after the `TLM_OK` line prints.
-
- **Anti-patterns that produce the ugly pyramid โ do NOT do these:**
- - โ Building landscape from `Part`s / `WedgePart`s stacked in layers โ hard rectangular steps (this is exactly the ziggurat look).
- - โ Concentric `FillBlock`/`FillBall` disks of shrinking size with no noise โ a smooth cone or a stepped pyramid.
- - โ Perfectly symmetric peaks, uniform slopes, five identical mountains.
+ Tune `peaks` (count/height/spread), `W`, `RES` (bump to 8 for huge volumes) and the three noise amplitudes (bigger = rougher). Keep peak centers inside `ยฑW/2`. **`WriteVoxels` caps at ~4.19M voxels per call** โ loop the region in โค~256-stud chunks for a bigger world.
- (If for some reason you cannot use `WriteVoxels` and must use `FillBall`, STILL drive each ball's radius and center offset with `math.noise` and overlap MANY small jittered balls so the silhouette is ragged โ but the heightmap + `WriteVoxels` above is strongly preferred for realism.)
+ **Never:** landscape from `Part`s/`WedgePart`s; concentric `FillBlock`/`FillBall` disks without noise; perfectly symmetric peaks or identical mountains. If you truly must use `FillBall`, still jitter every radius/center with `math.noise` and overlap many small balls โ but the heightmap above is strongly preferred.
- ## GENERATIVE tools (mesh / model / material)
+ ## GENERATIVE tools
- `generate_mesh` / `generate_procedural_model` / `generate_material` kick off an ASYNC job and return a job id. Then `wait_job_finished(job_id)` โ place the result with Luau. Use these for organic PROPS (rocks, trees, creatures) โ not for large landscape.
+ `generate_mesh` / `generate_procedural_model` / `generate_material` start an ASYNC job returning a job id โ `wait_job_finished(job_id)` โ place the result with Luau. For organic PROPS, not landscape.
## VERIFY & REPORT (always)
- 1. `get_console_output` โ confirm the `TLM_OK` marker printed and there are no red errors / no `TLM_FAIL`.
- 2. `screen_capture` โ aim the camera at the terrain first (`workspace.CurrentCamera.CFrame = CFrame.lookAt(Vector3.new(400,300,400), Vector3.new(0,60,0))` via a quick `execute_luau`), then capture, so you and the user actually SEE that it is natural, not blocky.
- 3. Tell the user in a couple of lines WHAT was built and HOW to see it (which place; select it in Explorer + press F to frame). Only claim success after verify confirms it.
+ 1. `get_console_output` โ the `TLM_OK` marker printed, no red errors, no `TLM_FAIL`.
+ 2. `screen_capture` โ aim the camera first via a quick `execute_luau` (`workspace.CurrentCamera.CFrame = CFrame.lookAt(Vector3.new(400,300,400), Vector3.new(0,60,0))`), so you both SEE it is natural, not blocky.
+ 3. Say in two lines WHAT was built and HOW to see it (which place; select in Explorer + press F). Claim success only after verifying.
## FAILURE HANDLING (honest, never silent)
- Studio not connected โ the STEP 0 message; do not pretend to build.
- - A Luau error / `TLM_FAIL` โ read `get_console_output`, fix the actual line (types, nil, WriteVoxels region/array sizing), retry ONCE. If it still fails, report the exact error and stop.
- - A generative job that never finishes โ report the timeout; fall back to `execute_luau` for deterministic geometry.
- - Do NOT report "done" unless the console/verify step actually confirmed it.
+ - Luau error / `TLM_FAIL` โ read the console, fix the real line (types, nil, WriteVoxels region/array sizing), retry ONCE, then report the exact error and stop.
+ - Generative job never finishes โ report the timeout, fall back to `execute_luau`.
+ - Never report "done" unless verify actually confirmed it.