Roblox place and model files
@lune/roblox reads .rbxl, .rbxlx, .rbxm and .rbxmx files into instances, lets you work with
them through the API you know from Roblox, and writes them back.
local fs = require("@lune/fs")local roblox = require("@lune/roblox")
local game = roblox.deserializePlace(fs.readFile("place.rbxl"))
local workspace = game:GetService("Workspace")for _, child in workspace:GetChildren() do print(child.ClassName, child.Name)end
fs.writeFile("place.rbxl", roblox.serializePlace(game))It is a file library, not an engine. Nothing is simulated or rendered, no events fire, and scripts in the place do not run.
Reading and writing files
Section titled “Reading and writing files”The four functions work on file contents, not on paths. Read and write the file itself with
@lune/fs.
| Function | Takes | Returns |
|---|---|---|
roblox.deserializePlace(contents) |
A place file’s contents | A DataModel |
roblox.deserializeModel(contents) |
A model file’s contents | A list of the root instances |
roblox.serializePlace(dataModel, xml) |
A DataModel |
The file contents as a string |
roblox.serializeModel(instances, xml) |
A list of instances | The file contents as a string |
Deserializing accepts both the binary and the XML format. Serializing writes binary unless xml is
true; pick the file extension to match.
local fs = require("@lune/fs")local roblox = require("@lune/roblox")
local roots = roblox.deserializeModel(fs.readFile("sword.rbxm"))local sword = roots[1]sword.Name = "Longsword"
fs.writeFile("longsword.rbxm", roblox.serializeModel({ sword }))fs.writeFile("longsword.rbxmx", roblox.serializeModel({ sword }, true))Instance references survive a round trip. An ObjectValue whose Value points at another instance
in the same file still points at it after serializing and deserializing, including across the roots
of a model and across services of a place.
Each deserialized file is its own independent tree. The same place read twice gives two separate
DataModel values that do not compare equal, and editing one does not affect the other.
Instances
Section titled “Instances”roblox.Instance.new(className) creates an instance. Its Name starts as the class name and its
Parent is nil.
local roblox = require("@lune/roblox")local Instance = roblox.Instance
local folder = Instance.new("Folder")folder.Name = "Assets"
local part = Instance.new("Part")part.Parent = folder
print(part:GetFullName()) --> Assets.Partprint(part:IsA("BasePart")) --> true- Class names are case-sensitive, and an unknown class throws.
- Classes that Roblox marks as not creatable, such as
BasePart, can be created here. - The usual methods are available:
Clone,Destroy,ClearAllChildren,GetChildren,GetDescendants,GetFullName,IsA,IsAncestorOf,IsDescendantOf, theFindFirstChildandFindFirstAncestorfamilies, and the attribute and tag methods below. Destroydestroys the instance and its descendants. Reading a property of a destroyed instance throws. Destroying the root of a deserialized file tears down the whole tree.
A new place starts from a DataModel. GetService returns the service, creating it when it is
missing; FindService returns nil instead of creating it.
local game = roblox.Instance.new("DataModel")
print(game:FindService("ReplicatedStorage")) --> nillocal storage = game:GetService("ReplicatedStorage")
local module = roblox.Instance.new("ModuleScript")module.Parent = storageProperties and datatypes
Section titled “Properties and datatypes”Properties are read and written by name, as on Roblox. A property that has never been set reads as the class default. A property the class does not have throws.
The typedefs know only Name, ClassName and Parent, and do not describe the datatypes. In
strict mode, cast the library or the instance to any to reach everything else.
local roblox = require("@lune/roblox") :: anylocal Instance = roblox.Instancelocal Vector3 = roblox.Vector3local CFrame = roblox.CFramelocal BrickColor = roblox.BrickColor
local part = Instance.new("Part")part.Anchored = truepart.Size = Vector3.new(4, 1, 2)part.CFrame = CFrame.new(0, 10, 0)part.BrickColor = BrickColor.Red()part.Shape = Enum.PartType.Ball
print(part.Transparency) --> 0, the defaultThe datatypes are fields of the library, named as on Roblox: Axes, BrickColor, CFrame,
Color3, ColorSequence, ColorSequenceKeypoint, Content, Enum, Faces, Font,
NumberRange, NumberSequence, NumberSequenceKeypoint, PhysicalProperties, Random, Ray,
Rect, Region3, Region3int16, UDim, UDim2, Vector2, Vector2int16, Vector3 and
Vector3int16. typeof returns the datatype’s name for their values.
Enum is also a global, as on Roblox, so a script reaches Enum.PartType.Ball without the library.
The global and roblox.Enum are the same value, and an item read through one is the same item as
through the other. Like the library, the typedefs leave it as any.
Random is a generator a script keeps for itself, never a property. Random.new(seed) steps the
same PCG32 generator as math.random: after math.randomseed(seed), math.random() and
math.random(min, max) give what NextNumber() and NextInteger(min, max) give.
A child can also be reached by indexing its parent with its name, such as game.Workspace. Prefer
FindFirstChild for names you do not control, since a name can clash with a property.
Attributes and tags
Section titled “Attributes and tags”local roblox = require("@lune/roblox") :: any
local folder = roblox.Instance.new("Folder")
folder:SetAttribute("Health", 100)folder:SetAttribute("Spawn", roblox.Vector3.new(0, 5, 0))print(folder:GetAttribute("Health")) --> 100
for name, value in folder:GetAttributes() do print(name, value)end
folder:SetAttribute("Health", nil) -- removes it
folder:AddTag("Checkpoint")print(folder:HasTag("Checkpoint")) --> trueprint(folder:GetTags())folder:RemoveTag("Checkpoint")- Attribute names may use letters, digits, periods, hyphens, underscores and forward slashes, and may be up to 100 characters long.
- Names that begin with
RBXmay be read and written. - Attribute values may be strings, numbers, booleans, and datatypes such as
Vector3,CFrame,Color3,UDim2,Fontand enum items. Clonecopies attributes along with properties.
Finding instances with QueryDescendants
Section titled “Finding instances with QueryDescendants”instance:QueryDescendants(selector) returns the descendants that match a selector, in depth-first
order. The instance itself is never included. The selector syntax and the results follow the Roblox
engine.
local workspace = game:GetService("Workspace")
local parts = workspace:QueryDescendants("BasePart")local spawns = workspace:QueryDescendants("Part.Spawn")local unanchored = workspace:QueryDescendants("[Anchored = false]")| Selector | Matches |
|---|---|
Part |
Instances for which IsA("Part") is true. Case-sensitive. |
.Fruit |
Instances with the tag Fruit. |
#RedTree |
Instances whose Name is exactly RedTree. |
[CanCollide = false] |
A property value, using the class default when it was never set. |
[Material = Plastic] |
An enum property, by item name. |
[$Variety] |
Instances that have the attribute Variety. |
[$Variety = Fuji] |
An attribute value. String comparison is case-sensitive. |
Model.Apple |
A compound: every part must match the same instance. |
Model > Part |
Parts that are direct children of a Model. |
Model >> Part |
Parts anywhere under a Model. |
> Part |
Direct children of the instance being queried. |
.Fruit, .SwordPart |
A list: the matches of each selector, one after the other. |
BasePart:not(MeshPart) |
Negation of a selector list. |
Model:has(.Fruit) |
Instances whose own descendants contain a match. |
Other things to know:
- An unknown class name, tag or name matches nothing. It is not an error.
- An empty selector matches nothing.
- A selector list does not remove duplicates: an instance matched by two selectors in the list appears twice.
- An unquoted value may not contain a decimal point:
[Transparency = 0.5]is an error. Double quotes widen the characters allowed, as in[$Flavor = "4.0"]; they do not force a string comparison. Single quotes are an error. - A property selector needs a value:
[Anchored]is an error. Only attributes can be tested for presence. - A boolean attribute matches only
trueorfalse, not1or0. - Comparing a property or attribute of a type that cannot be compared, such as a
Color3or aVector3, is an error. - A malformed selector throws: an unterminated bracket or parenthesis, an unknown pseudo-class,
*, or a combinator with nothing after it.
The reflection database
Section titled “The reflection database”roblox.getReflectionDatabase() returns the bundled description of Roblox’s classes and enums. It
is what the library uses to know which properties exist and what their defaults are.
local roblox = require("@lune/roblox")
local db = roblox.getReflectionDatabase()print(db.Version)print(#db:GetClassNames(), "classes,", #db:GetEnumNames(), "enums")
local class = db:GetClass("Part")if class then print(class.Superclass) for name, prop in class.Properties do print(name, prop.Datatype, prop.Scriptability) endend
local enum = db:GetEnum("PartType")if enum then for name, value in enum.Items do print(name, value) endendGetClassandGetEnumneed the exact name.FindClassandFindEnumignore case and surrounding whitespace. All four return nil when nothing is found.class.Propertieslists only the properties the class itself declares. WalkSuperclassto collect inherited ones.class.DefaultPropertiesmaps property names to default values, andclass.Tagsholds tags such as"Service"and"NotCreatable".enum.Itemsmaps item names to their numbers. It does not holdEnumItemvalues.
Adding properties and methods
Section titled “Adding properties and methods”The library implements what a file needs, not the engine’s behaviour. A method such as
HttpService:GetAsync does not exist until you supply it. roblox.implementMethod and
roblox.implementProperty add members to a class and to every class that inherits from it.
local net = require("@lune/net")local roblox = require("@lune/roblox")local serde = require("@lune/serde")
roblox.implementMethod("HttpService", "GetAsync", function(_, url: string) return net.request(url).bodyend)
roblox.implementMethod("HttpService", "JSONDecode", function(_, value: string) return serde.decode("json", value)end)
local game = roblox.Instance.new("DataModel")local HttpService = game:GetService("HttpService") :: anylocal data = HttpService:JSONDecode(HttpService:GetAsync("https://httpbingo.org/json"))The callback receives the instance first, then the arguments the caller passed. Whatever it returns
is returned to the caller. It may call yielding functions, as GetAsync does above.
implementProperty takes a getter and an optional setter. Without a setter the property is
read-only, and assigning to it throws.
local roblox = require("@lune/roblox")
local labels = {}
roblox.implementProperty("BasePart", "Label", function(instance) return labels[instance:GetDebugId()] or ""end, function(instance, value) labels[instance:GetDebugId()] = valueend)
local part = roblox.Instance.new("Part") :: anypart.Label = "floor"print(part.Label) --> floorAdding classes and services
Section titled “Adding classes and services”Some classes exist only in particular Roblox builds and are missing from the reflection database.
roblox.registerClass(className, superClassName) adds one. The superclass defaults to "Instance",
and may be a built-in class or one registered earlier.
local roblox = require("@lune/roblox")
roblox.registerClass("Inventory")roblox.registerClass("Backpack2", "Inventory")
roblox.implementMethod("Inventory", "Count", function(instance) return #instance:GetChildren()end)
local pack = roblox.Instance.new("Backpack2") :: anyprint(pack:IsA("Inventory")) --> trueprint(pack:Count()) --> 0roblox.registerService(serviceName) does the same and marks the class as a service, so that
GetService creates and returns it. A class registered with registerClass is not a service, and
GetService throws for it.
local fs = require("@lune/fs")local roblox = require("@lune/roblox")
roblox.registerService("FileSystemService")roblox.implementMethod("FileSystemService", "ReadFile", function(_, path: string) return fs.readFile(path)end)
local game = roblox.Instance.new("DataModel")local files = game:GetService("FileSystemService") :: anyprint(files:ReadFile("README.md"))Registering a name that already exists throws, whether it is built in or was registered before. So does a superclass that does not exist.
Roblox web APIs and Studio paths
Section titled “Roblox web APIs and Studio paths”roblox.getAuthCookie() returns the Roblox auth cookie of the logged-in user, or nil when none is
found. The value is formatted for use as a Cookie header and is restricted to official Roblox
endpoints. Pass true to get the raw cookie value instead.
local net = require("@lune/net")local roblox = require("@lune/roblox")
local cookie = roblox.getAuthCookie()assert(cookie ~= nil, "not logged in to Roblox on this machine")
local response = net.request({ url = "https://assetdelivery.roblox.com/v2/assetId/1234567890", headers = { Cookie = cookie },})Four functions return where Roblox Studio keeps its files on this machine:
roblox.studioApplicationPath(), roblox.studioContentPath(), roblox.studioPluginPath() and
roblox.studioBuiltinPluginPath(). The application path is where Studio would be; it is not checked
to exist. The plugin folder may be missing when no local plugin was ever installed.
local fs = require("@lune/fs")local roblox = require("@lune/roblox")
local plugin = roblox.Instance.new("Folder")plugin.Name = "MyPlugin"
fs.writeFile(roblox.studioPluginPath() .. "/MyPlugin.rbxm", roblox.serializeModel({ plugin }))Reference
Section titled “Reference”The full listing is on the roblox reference. Reading and writing the files themselves is covered in files and data formats, and requests to Roblox’s web APIs in networking.
