A native Linux, CLI-only, offline-first Roblox Studio alternative designed for AI coding agents.
Status: v0.2.0-beta.1; Phases 1–6, 8–24 implemented (Phase 7, browser play, is intentionally skipped). Editing core, place templates, real headless Vulkan rendering, a Roblox GUI engine with RichText and nine-slice images, an MCP server for AI agents, the official Luau runtime, client/server simulation, and Jolt-powered physics playtest with R6/R15 characters. Example output: castle-day.png · hud-desktop.png · hud-mobile.png · play-r6.png (regenerate scene/UI shots with
./scripts/make_screenshots.sh). Everything is verified by internal automated tests only — this environment has no Roblox Studio, and no 1:1 parity is claimed. See COMPATIBILITY.md.
blox studio start|status|save|stop <place.rbxl>— persistent editing session: one engine serves many CLI/MCP invocations, unsaved edits stay in memory, undo/redo works across commands, killed sessions recover from atomic snapshots (see Phase 25 sessions).- Terrain Luau APIs (
FillCylinder,WriteVoxels,Region3), real TweenService (TweenInfo+ easing),TouchEnded, and an offline DataStoreService emulation (<place>.bloxdata/) — see geometry/serialization, GUI fidelity and services. No Roblox cloud access is claimed.
JSON-RPC 2.0 (newline-delimited, protocol "1")
bloxcli (Rust) ───────────────────────────────────────────► blox-engine (C++20)
├─ CLI parsing stdin/stdout pipes ├─ DataModel (source of truth)
├─ .rbxl/.rbxlx I/O ◄─────────────────────────────────── ├─ reflection defaults + validation
│ via rbx-dom crates ├─ hierarchy ops + transform math
└─ undo/redo snapshots (.bloxhistory/) └─ instance tree, typed properties
- C++20 engine (
engine/): owns the DataModel at runtime. Instance tree, typed property values, reflection defaults for the building core, path resolution (duplicate sibling names are errors, never silent first matches), hierarchy operations, world-space CFrame transform math (Model pivots, bounding boxes), validation, and snapshot load/save. Built with CMake + Ninja. No third-party dependencies. - Rust CLI (
crates/bloxcli/): argument parsing, structured--jsonoutput for agents, undo/redo history, and place file I/O through the upstream rbx-dom crates — the same serializers the Rojo ecosystem uses:rbx_binary3.0.1,rbx_xml3.0.1,rbx_dom_weak4.2.0,rbx_types3.1.0,rbx_reflection_database3.0.1 (Roblox 741). Property types are coerced to Roblox's reflection types before writing, so files carry Studio-correct property types, and validation checks every property against the official reflection database. - IPC: the CLI spawns
blox-engineper command, performs a versionedinitializehandshake (protocol"1"), loads the place into the engine, mutates it through the engine, then snapshots and writes the file back atomically (temp file + rename; a failed command never modifies the file). The engine is a persistent process for the whole session and also runs standalone untilshutdownor EOF. Methods:initialize,ping,datamodel/new,datamodel/load,datamodel/snapshot,instance/add,instance/get,instance/set,instance/clone,instance/delete,instance/reparent,instance/group,instance/find,instance/move,instance/rotate,instance/scale,instance/pivot,validate,render/screenshot,render/camera,ui/layout,ui/validate,ui/render,shutdown. - Canonical tree JSON is the shared contract between the two halves
(see
crates/bloxcli/src/tree.rsandengine/include/blox/variant.hpp). Supported value types:Bool,Int32,Int64,Float32,Float64,String,BinaryString(base64),Vector3,Color3,CFrame,Enum,EnumItem,BrickColor,ContentId,Tags,Attributes,Vector2,Vector2int16,Vector3int16,UDim,UDim2,NumberRange,Rect,Ray,Faces,Axes,PhysicalProperties,ColorSequence,NumberSequence, andRef(stored as a child-index path and rewired to a real referent on write, so e.g.Model.PrimaryPartsurvives edits). A file containing any other property type fails loudly with the class, property, and type named — data is never silently dropped. - Undo/redo: every mutating command first snapshots the place file
into a sibling
<place>.bloxhistory/directory (raw file snapshots +state.json, capped at 100).undo/redorestore snapshots only after they decode successfully. History is file-level, so it works across CLI processes; it is not stored inside the.rbxl. - Referents: canonical referents are assigned in traversal order on
load in both the Rust conversion and the engine, so an instance keeps
its
#id(shown bytree, returned byget/find) across a load/save of an unchanged tree. Structural edits can renumber later instances — name paths remain the stable address.
Prerequisites (Ubuntu/Debian):
sudo apt-get install -y build-essential cmake ninja-build cargo rustc \
libvulkan-dev libpng-dev libfreetype-dev glslang-tools mesa-vulkan-driversRendering needs a Vulkan driver; Mesa's Lavapipe (mesa-vulkan-drivers)
provides software Vulkan on machines without a GPU. Shaders are compiled
to SPIR-V by glslangValidator at build time and embedded in the engine.
Build and test:
./scripts/build.sh # CMake/Ninja for C++, Cargo for Rust
./scripts/test.sh # CTest (engine) + Cargo integration testsThe CLI finds the engine via --engine <path>, $BLOX_ENGINE, a
blox-engine sitting next to the CLI binary, or build/engine/blox-engine.
The same binary is also installed as blox, so blox get … works as
written in the command list.
Download/extract the Linux tarball (built by scripts/package-release.sh,
also produced by CI), then:
tar -xzf rbxfoundry-0.2.0-beta.1-linux-x86_64.tar.gz
cd rbxfoundry-0.2.0-beta.1-linux-x86_64
./bin/bloxcli --version
./bin/bloxcli new myplace.rbxl --template baseplate
./bin/bloxcli render myplace.rbxl --output scene.pngThe tarball is self-contained: release binaries plus bundled DejaVu
fonts under bin/fonts/ (the engine also honours $BLOX_FONT and
ui render --font <path>). A Vulkan driver is still required for
rendering (Mesa Lavapipe works without a GPU). See
docs/TROUBLESHOOTING.md and
docs/BENCHMARKS.md. Examples:
examples/README.md.
B=./target/debug/bloxcli
$B new game.rbxl --name MyPlace # place with the standard services
$B add game.rbxl --parent Workspace --class Part --name Crate
$B move game.rbxl --target Workspace/Crate --position "10,5,0"
$B clone game.rbxl --target Workspace/Crate --name Crate2
$B group game.rbxl --targets Workspace/Crate,Workspace/Crate2 --name Crates
$B rotate game.rbxl --target Workspace/Crates --angles "0,90,0"
$B set game.rbxl --target Workspace/Crates/Crate --property Material --value Wood
$B find game.rbxl --class Part
$B tree game.rbxl --props
$B validate game.rbxl
$B undo game.rbxl # un-does the last mutation
$B export game.rbxl game.rbxlx # genuine binary <-> XML conversion
$B --json get game.rbxl --target Workspace/Crates --property WorldPivot
$B render game.rbxl --output scene.png --camera "30,25,30" --look-at "0,5,0"
$B camera game.rbxl # CurrentCamera / bounds / suggested framing
$B --json render game.rbxl --output s.png --probe "400,300;400,500" # exact frame pixels
$B ui render game.rbxl --output hud.png --device mobile
$B ui inspect game.rbxl --json # computed rects, visibility, render order
$B ui validate game.rbxl --json # offscreen / clipped / zero-size elements
$B ui hit-test game.rbxl --x 250 --y 400 --json # simulation only; no scripts runCommands:
| Command | Behaviour |
|---|---|
new <file> [--name N] [--force] |
New place with the standard service set (Workspace, Lighting, ReplicatedStorage, ServerStorage, ServerScriptService, StarterGui, StarterPlayer, StarterPack, Teams, SoundService). Refuses to overwrite without --force. |
inspect <file> [--path P] [--props] |
Phase 1 alias of tree. |
add <file> --parent P --class C [--name N] |
Adds an instance with engine reflection defaults (BasePart family, MeshPart, Model, Folder, SpawnLocation, scripts, value objects, services…). Any class name is accepted. |
set <file> --target P --property N --value V [--type T] |
Sets a typed property. Type from the existing property, or --type for new ones (includes UDim, UDim2, Vector2, NumberRange, Rect, Faces, Axes, PhysicalProperties, ColorSequence as t:r,g,b;…, NumberSequence as t:v,e;…, EnumItem as Type:value). Name renames; Position stays in sync with CFrame. |
save <file> |
Loads, engine-validates, canonically rewrites. Refuses an invalid tree. |
export <in> <out> [--force] |
Converts .rbxl <-> .rbxlx (or re-encodes the same format). |
validate <file> |
Full decode + engine validation + reflection-database type validation. Exit 1 when invalid. |
diff <a> <b> |
Semantic, property-level DataModel diff of two places/models (Phase 18). Exit 1 when differences exist. |
imagediff <a.png> <b.png> [--heatmap H] [--tolerance F] |
Pixel metrics (MAE/RMSE/diff fraction) + optional heatmap (Phase 18). |
| `model export | inspect |
| `place info | manifest ` |
| `template list | inspect |
parity dashboard |
Static HTML compatibility dashboard from compat-report.json. |
ungroup <file> --target <model> |
Move a container's children to its parent and delete it (also MCP instance_ungroup). |
set --targets a,b,c |
Bulk property edit across a multi-selection. |
move --snap <studs> / rotate --snap-angle <deg> |
Grid and angle snapping. |
exec <file> --code <luau> |
Studio Command Bar: Luau in the edit context, committed to the file. |
| `terrain flatten | smooth |
| `script analyze | complete` |
| `debug start | breakpoint |
play input --click/--hover/--type-text |
GUI pointer/text input firing real LocalScript GUI events. |
get <file> --target P [--property N] |
One instance (referent, properties, children) or one typed property. Position, Name, ClassName derived. |
tree <file> [--path P] [--depth N] [--props] |
Hierarchy with stable referents (#id). |
find <file> [--name N] [--class C] [--under P] [--contains] |
Search by exact/substring name and/or class. |
clone <file> --target P [--name N] [--parent P] |
Deep-copy with fresh referents. |
delete <file> --target P |
Deletes a subtree. Root deletion rejected. |
reparent <file> --target P --parent P2 |
Moves an instance. Cycles/root moves rejected. |
group <file> --targets A,B,… [--name N] [--parent P] |
Groups under a new Model; world transforms preserved. Default parent: first target's parent. |
move <file> --target P (--delta 'x,y,z' [--relative] | --position 'x,y,z') |
BasePart or whole Model. --relative = local axes; --position absolute (single BasePart). |
rotate <file> --target P --angles 'rx,ry,rz' [--world] |
Degrees, intrinsic XYZ. Parts about their centre, Models about their pivot. |
scale <file> --target P (--factor F | --factors 'x,y,z' | --size 'x,y,z') |
BasePart per-axis, or Model uniform about pivot (Model:ScaleTo semantics; non-uniform Model scale rejected). |
pivot <file> --target Model [--position 'x,y,z'] |
Get effective pivot (explicit WorldPivot, else bounding-box centre) or set it. |
undo <file> / redo <file> |
History in <file>.bloxhistory/ (100 snapshots). Fresh mutation clears redo. |
render <file> --output P.png [--camera 'x,y,z'] [--look-at 'x,y,z'] [--fov D] [--ortho] [--preset P] [--quality low|medium|high] [--no-shadows] [--probe 'x,y;…'] |
Real headless Vulkan render of Workspace geometry to PNG (Phase 3). --probe returns exact frame pixels in JSON. Missing mesh/texture assets are reported, never faked. |
camera <file> |
Workspace.CurrentCamera if present, scene bounds, suggested framing camera. |
ui render|screenshot <file> --output P.png [--device desktop|laptop|tablet|mobile] [--with-scene] |
Rasterises actual GUI instances (layouts, text, images) at device/custom sizes (Phase 4). |
ui inspect <file> |
Computed absolute layout rects, visibility, render order. |
ui validate <file> |
Flags offscreen (error), clipped, zero-size, invisible elements. Exit 1 on errors. |
ui hit-test <file> --x X --y Y |
Topmost element at a point + events it would fire (hit resolution only; handlers run under run/play). |
run <file> [--duration S] [--eval <luau>] [--save-runtime O] [--render P] [--ui P] [--commit] |
Executes the place's Luau scripts on a runtime copy (official Luau VM), steps simulated time, reports logs + tracebacks. Edit state untouched unless --commit. |
| `play start | step |
| `net start | step |
| `terrain fill | ball |
mcp [--root <dir>] |
MCP server over stdio (36 tools: editor, render, UI, runtime, play, net). Filesystem confined to --root. |
All commands accept the global --json flag. Mutating commands record an
undo snapshot before writing and write atomically.
- Runtime is Luau, physics is Jolt — both real, both partial.
Scripting runs the official Luau VM against a documented subset of
the Roblox API (no DataStores; client/server split exists in
netsessions — see COMPATIBILITY.md); physics covers rigid bodies and character capsules, not constraints or Humanoid solver parity. See COMPATIBILITY.md for exact subsets. - Rendering is real but not Studio-identical. Procedural material patterns instead of Studio's PBR texture maps, gradient sky instead of skybox cubemaps, a single shadow cascade, two-sided rendering (no backface culling), DejaVu Sans substituted for Roblox's non-redistributable fonts. Mesh v5 geometry renders (skinning does not, and says so); online-only assets without a local copy are reported, never faked. Full matrix in COMPATIBILITY.md.
- Models are single-root only.
.rbxm/.rbxmxexport/import handles one subtree root; references leaving the subtree fail loudly. See COMPATIBILITY.md. - Property types. The canonical set above covers the common Studio
property types. Files using still-unsupported types (e.g.
Region3,MaterialColors, object-referencingContent) fail loudly at load with class/property/type named rather than losing data.Color3quantizes to 1/255 in the binary format, as in Roblox. - Model pivots. Roblox file serializers do not store
Model.WorldPivot. BloxCLI persists an explicitly set pivot in a plain CFrame attribute namedBloxPivot(visible and removable in Studio) and restores it asWorldPivoton load. Without one, the effective pivot is the descendant-parts bounding-box centre. Deleting that attribute in Studio resets the pivot to the bounding-box centre. - Referents are file-order identifiers, not Studio GUIDs; structural edits can renumber later instances. Use name paths as the stable address. Duplicate sibling names make paths ambiguous (reported, never silently resolved).
- Undo history is per place file and snapshot-based; concurrent CLI writers are last-writer-wins.
- Reflection defaults cover the building core. Other classes get
correct file types via the reflection database on write, but no
engine-side default property set. Terrain and CSG geometry are not
generated (preserved as data only). Luau/Jolt are compiled from
vendored sources in
third_party/(official Luau release/744, Jolt Physics v5.2.0; both MIT-licensed). - Studio itself has never been run against these files in this environment; compatibility is established through the upstream rbx-dom serializers, the official reflection database, byte-level checks, and round-trip tests — not through a Studio comparison.