Migrate roblox builtin functions for place & model files to more flexible APIs

This commit is contained in:
Filip Tibell 2023-05-20 14:23:51 +02:00
parent 6628220429
commit 2297350c6e
No known key found for this signature in database
13 changed files with 174 additions and 118 deletions

View file

@ -10,6 +10,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Breaking Changes
- Migrated several functions in the `roblox` builtin to new, more flexible APIs:
- `readPlaceFile -> deserializePlace`
- `readModelFile -> deserializeModel`
- `writePlaceFile -> serializePlace`
- `writeModelFile -> serializeModel`
These new APIs **_no longer use file paths_**, meaning to use them with files you must first read them using the `fs` builtin.
- Removed `CollectionService` and its methods from the `roblox` builtin library - new instance methods have been added as replacements.
- Removed [`Instance:FindFirstDescendant`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstDescendant) which was a method that was never enabled in the official Roblox API and will soon be removed. <br/>
Use the second argument of the already existing find methods instead to find descendants.
### Added ### Added
- Added `serde.compress` and `serde.decompress` for compressing and decompressing strings using one of several compression formats: `brotli`, `gzip`, `lz4`, or `zlib`. - Added `serde.compress` and `serde.decompress` for compressing and decompressing strings using one of several compression formats: `brotli`, `gzip`, `lz4`, or `zlib`.
@ -38,12 +53,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [`Instance:RemoveTag`](https://create.roblox.com/docs/reference/engine/classes/Instance#RemoveTag) - [`Instance:RemoveTag`](https://create.roblox.com/docs/reference/engine/classes/Instance#RemoveTag)
- Implemented the second argument of the `FindFirstChild` / `FindFirstChildOfClass` / `FindFirstChildWhichIsA` instance methods. - Implemented the second argument of the `FindFirstChild` / `FindFirstChildOfClass` / `FindFirstChildWhichIsA` instance methods.
### Removed
- Removed `CollectionService` and its methods from the `roblox` builtin library.
- Removed [`Instance:FindFirstDescendant`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstDescendant) which was a method that was never enabled in the official Roblox API and will soon be removed. <br/>
Use the second argument of the already existing find methods instead to find descendants.
### Changed ### Changed
- Update to Luau version `0.576` - Update to Luau version `0.576`

View file

@ -9,17 +9,22 @@ export type Instance = {}
### Example usage ### Example usage
```lua ```lua
local fs = require("@lune/fs")
local roblox = require("@lune/roblox") local roblox = require("@lune/roblox")
-- Reading & writing a place file -- Reading a place file
local game = roblox.readPlaceFile("myPlaceFile.rbxl") local placeFile = fs.readFile("myPlaceFile.rbxl")
local workspace = game:GetService("Workspace") local game = roblox.deserializePlace(placeFile)
-- Manipulating and reading instances - just like in Roblox!
local workspace = game:GetService("Workspace")
for _, child in workspace:GetChildren() do for _, child in workspace:GetChildren() do
print("Found child " .. child.Name .. " of class " .. child.ClassName) print("Found child " .. child.Name .. " of class " .. child.ClassName)
end end
roblox.writePlaceFile("myPlaceFile.rbxl", game) -- Writing a place file
local newPlaceFile = roblox.serializePlace(game)
fs.writeFile("myPlaceFile.rbxl", newPlaceFile)
``` ```
]=] ]=]
return { return {
@ -27,70 +32,100 @@ return {
@within Roblox @within Roblox
@must_use @must_use
Reads a place file into a DataModel instance. Deserializes a place into a DataModel instance.
This function accepts a string of contents, *not* a file path.
If reading a place file from a file path is desired, `fs.readFile`
can be used and the resulting string may be passed to this function.
### Example usage ### Example usage
```lua ```lua
local fs = require("@lune/fs")
local roblox = require("@lune/roblox") local roblox = require("@lune/roblox")
local game = roblox.readPlaceFile("filePath.rbxl")
local placeFile = fs.readFile("filePath.rbxl")
local game = roblox.deserializePlace(placeFile)
``` ```
@param filePath The file path to read from @param contents The contents of the place to read
]=] ]=]
readPlaceFile = function(filePath: string): Instance deserializePlace = function(contents: string): Instance
return nil :: any return nil :: any
end, end,
--[=[ --[=[
@within Roblox @within Roblox
@must_use @must_use
Reads a model file into a table of instances. Deserializes a model into an array of instances.
This function accepts a string of contents, *not* a file path.
If reading a model file from a file path is desired, `fs.readFile`
can be used and the resulting string may be passed to this function.
### Example usage ### Example usage
```lua ```lua
local fs = require("@lune/fs")
local roblox = require("@lune/roblox") local roblox = require("@lune/roblox")
local instances = roblox.readModelFile("filePath.rbxm")
local modelFile = fs.readFile("filePath.rbxm")
local instances = roblox.deserializeModel(modelFile)
``` ```
@param filePath The file path to read from @param contents The contents of the model to read
]=] ]=]
readModelFile = function(filePath: string): { Instance } deserializeModel = function(contents: string): { Instance }
return nil :: any return nil :: any
end, end,
--[=[ --[=[
@within Roblox @within Roblox
@must_use
Writes a DataModel instance to a place file. Serializes a place from a DataModel instance.
This string can then be written to a file, or sent over the network.
### Example usage ### Example usage
```lua ```lua
local fs = require("@lune/fs")
local roblox = require("@lune/roblox") local roblox = require("@lune/roblox")
roblox.writePlaceFile("filePath.rbxl", game)
local placeFile = roblox.serializePlace(game)
fs.writeFile("filePath.rbxl", placeFile)
``` ```
@param filePath The file path to write to @param dataModel The DataModel for the place to serialize
@param dataModel The DataModel to write to the file @param xml If the place should be serialized as xml or not. Defaults to `false`, meaning the place gets serialized using the binary format and not xml.
]=] ]=]
writePlaceFile = function(filePath: string, dataModel: Instance) end, serializePlace = function(dataModel: Instance, xml: boolean?): string
return nil :: any
end,
--[=[ --[=[
@within Roblox @within Roblox
@must_use
Writes one or more instances to a model file. Serializes one or more instances as a model.
This string can then be written to a file, or sent over the network.
### Example usage ### Example usage
```lua ```lua
local fs = require("@lune/fs")
local roblox = require("@lune/roblox") local roblox = require("@lune/roblox")
roblox.writeModelFile("filePath.rbxm", { instance1, instance2, ... })
local modelFile = roblox.serializeModel({ instance1, instance2, ... })
fs.writeFile("filePath.rbxm", modelFile)
``` ```
@param filePath The file path to write to @param instances The array of instances to serialize
@param instances The array of instances to write to the file @param xml If the model should be serialized as xml or not. Defaults to `false`, meaning the model gets serialized using the binary format and not xml.
]=] ]=]
writeModelFile = function(filePath: string, instances: { Instance }) end, serializeModel = function(instances: { Instance }, xml: boolean?): string
return nil :: any
end,
--[=[ --[=[
@within Roblox @within Roblox
@must_use @must_use

View file

@ -1,8 +1,5 @@
use std::path::PathBuf;
use blocking::unblock; use blocking::unblock;
use mlua::prelude::*; use mlua::prelude::*;
use tokio::fs;
use lune_roblox::{ use lune_roblox::{
document::{Document, DocumentError, DocumentFormat, DocumentKind}, document::{Document, DocumentError, DocumentFormat, DocumentKind},
@ -19,36 +16,19 @@ pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
} }
TableBuilder::new(lua)? TableBuilder::new(lua)?
.with_values(roblox_constants)? .with_values(roblox_constants)?
.with_async_function("readPlaceFile", read_place_file)? .with_async_function("deserializePlace", deserialize_place)?
.with_async_function("readModelFile", read_model_file)? .with_async_function("deserializeModel", deserialize_model)?
.with_async_function("writePlaceFile", write_place_file)? .with_async_function("serializePlace", serialize_place)?
.with_async_function("writeModelFile", write_model_file)? .with_async_function("serializeModel", serialize_model)?
.with_async_function("getAuthCookie", get_auth_cookie)? .with_async_function("getAuthCookie", get_auth_cookie)?
.build_readonly() .build_readonly()
} }
fn parse_file_path(path: String) -> LuaResult<(PathBuf, DocumentFormat)> { async fn deserialize_place<'lua>(
let file_path = PathBuf::from(path); lua: &'lua Lua,
let file_ext = file_path contents: LuaString<'lua>,
.extension() ) -> LuaResult<LuaValue<'lua>> {
.ok_or_else(|| { let bytes = contents.as_bytes().to_vec();
LuaError::RuntimeError(format!(
"Missing file extension for file path: '{}'",
file_path.display()
))
})?
.to_string_lossy();
let doc_format = DocumentFormat::from_extension(&file_ext).ok_or_else(|| {
LuaError::RuntimeError(format!(
"Invalid file extension for writing place file: '{}'",
file_ext
))
})?;
Ok((file_path, doc_format))
}
async fn read_place_file(lua: &Lua, path: String) -> LuaResult<LuaValue> {
let bytes = fs::read(path).await.map_err(LuaError::external)?;
let fut = unblock(move || { let fut = unblock(move || {
let doc = Document::from_bytes(bytes, DocumentKind::Place)?; let doc = Document::from_bytes(bytes, DocumentKind::Place)?;
let data_model = doc.into_data_model_instance()?; let data_model = doc.into_data_model_instance()?;
@ -57,8 +37,11 @@ async fn read_place_file(lua: &Lua, path: String) -> LuaResult<LuaValue> {
fut.await?.to_lua(lua) fut.await?.to_lua(lua)
} }
async fn read_model_file(lua: &Lua, path: String) -> LuaResult<LuaValue> { async fn deserialize_model<'lua>(
let bytes = fs::read(path).await.map_err(LuaError::external)?; lua: &'lua Lua,
contents: LuaString<'lua>,
) -> LuaResult<LuaValue<'lua>> {
let bytes = contents.as_bytes().to_vec();
let fut = unblock(move || { let fut = unblock(move || {
let doc = Document::from_bytes(bytes, DocumentKind::Model)?; let doc = Document::from_bytes(bytes, DocumentKind::Model)?;
let instance_array = doc.into_instance_array()?; let instance_array = doc.into_instance_array()?;
@ -67,32 +50,36 @@ async fn read_model_file(lua: &Lua, path: String) -> LuaResult<LuaValue> {
fut.await?.to_lua(lua) fut.await?.to_lua(lua)
} }
async fn write_place_file(_: &Lua, (path, data_model): (String, Instance)) -> LuaResult<()> { async fn serialize_place(
let (file_path, doc_format) = parse_file_path(path)?; lua: &Lua,
(data_model, as_xml): (Instance, Option<bool>),
) -> LuaResult<LuaString> {
let fut = unblock(move || { let fut = unblock(move || {
let doc = Document::from_data_model_instance(data_model)?; let doc = Document::from_data_model_instance(data_model)?;
let bytes = doc.to_bytes_with_format(doc_format)?; let bytes = doc.to_bytes_with_format(match as_xml {
Some(true) => DocumentFormat::Xml,
_ => DocumentFormat::Binary,
})?;
Ok::<_, DocumentError>(bytes) Ok::<_, DocumentError>(bytes)
}); });
let bytes = fut.await?; let bytes = fut.await?;
fs::write(file_path, bytes) lua.create_string(&bytes)
.await
.map_err(LuaError::external)?;
Ok(())
} }
async fn write_model_file(_: &Lua, (path, instances): (String, Vec<Instance>)) -> LuaResult<()> { async fn serialize_model(
let (file_path, doc_format) = parse_file_path(path)?; lua: &Lua,
(instances, as_xml): (Vec<Instance>, Option<bool>),
) -> LuaResult<LuaString> {
let fut = unblock(move || { let fut = unblock(move || {
let doc = Document::from_instance_array(instances)?; let doc = Document::from_instance_array(instances)?;
let bytes = doc.to_bytes_with_format(doc_format)?; let bytes = doc.to_bytes_with_format(match as_xml {
Some(true) => DocumentFormat::Xml,
_ => DocumentFormat::Binary,
})?;
Ok::<_, DocumentError>(bytes) Ok::<_, DocumentError>(bytes)
}); });
let bytes = fut.await?; let bytes = fut.await?;
fs::write(file_path, bytes) lua.create_string(&bytes)
.await
.map_err(LuaError::external)?;
Ok(())
} }
async fn get_auth_cookie(_: &Lua, raw: Option<bool>) -> LuaResult<Option<String>> { async fn get_auth_cookie(_: &Lua, raw: Option<bool>) -> LuaResult<Option<String>> {

View file

@ -119,10 +119,10 @@ create_tests! {
roblox_datatype_vector3: "roblox/datatypes/Vector3", roblox_datatype_vector3: "roblox/datatypes/Vector3",
roblox_datatype_vector3int16: "roblox/datatypes/Vector3int16", roblox_datatype_vector3int16: "roblox/datatypes/Vector3int16",
roblox_files_read_model: "roblox/files/readModelFile", roblox_files_deserialize_model: "roblox/files/deserializeModel",
roblox_files_read_place: "roblox/files/readPlaceFile", roblox_files_deserialize_place: "roblox/files/deserializePlace",
roblox_files_write_model: "roblox/files/writeModelFile", roblox_files_serialize_model: "roblox/files/serializeModel",
roblox_files_write_place: "roblox/files/writePlaceFile", roblox_files_serialize_place: "roblox/files/serializePlace",
roblox_instance_attributes: "roblox/instance/attributes", roblox_instance_attributes: "roblox/instance/attributes",
roblox_instance_new: "roblox/instance/new", roblox_instance_new: "roblox/instance/new",

View file

@ -7,8 +7,11 @@ for _, dirName in fs.readDir("tests/roblox/rbx-test-files/places") do
end end
for _, modelDir in modelDirs do for _, modelDir in modelDirs do
local modelBinary = roblox.readModelFile(modelDir .. "/binary.rbxl") local modelFileBinary = fs.readFile(modelDir .. "/binary.rbxl")
local modelXml = roblox.readModelFile(modelDir .. "/xml.rbxlx") local modelFileXml = fs.readFile(modelDir .. "/xml.rbxlx")
local modelBinary = roblox.deserializeModel(modelFileBinary)
local modelXml = roblox.deserializeModel(modelFileXml)
for _, modelInstance in modelBinary do for _, modelInstance in modelBinary do
assert(modelInstance:IsA("Instance")) assert(modelInstance:IsA("Instance"))

View file

@ -7,8 +7,11 @@ for _, dirName in fs.readDir("tests/roblox/rbx-test-files/places") do
end end
for _, placeDir in placeDirs do for _, placeDir in placeDirs do
local placeBinary = roblox.readPlaceFile(placeDir .. "/binary.rbxl") local placeFileBinary = fs.readFile(placeDir .. "/binary.rbxl")
local placeXml = roblox.readPlaceFile(placeDir .. "/xml.rbxlx") local placeFileXml = fs.readFile(placeDir .. "/xml.rbxlx")
local placeBinary = roblox.deserializePlace(placeFileBinary)
local placeXml = roblox.deserializePlace(placeFileXml)
assert(placeBinary.ClassName == "DataModel") assert(placeBinary.ClassName == "DataModel")
assert(placeXml.ClassName == "DataModel") assert(placeXml.ClassName == "DataModel")

View file

@ -1,3 +1,4 @@
local fs = require("@lune/fs")
local roblox = require("@lune/roblox") :: any local roblox = require("@lune/roblox") :: any
local Instance = roblox.Instance local Instance = roblox.Instance
@ -6,11 +7,17 @@ local instances = {
Instance.new("Part"), Instance.new("Part"),
} }
roblox.writeModelFile("bin/temp-model.rbxm", instances) local modelAsBinary = roblox.serializeModel(instances)
roblox.writeModelFile("bin/temp-model.rbxmx", instances) local modelAsXml = roblox.serializeModel(instances, true)
local savedBinary = roblox.readModelFile("bin/temp-model.rbxm") fs.writeFile("bin/temp-model.rbxm", modelAsBinary)
local savedXml = roblox.readModelFile("bin/temp-model.rbxmx") fs.writeFile("bin/temp-model.rbxmx", modelAsXml)
local savedFileBinary = fs.readFile("bin/temp-model.rbxm")
local savedFileXml = fs.readFile("bin/temp-model.rbxmx")
local savedBinary = roblox.deserializeModel(savedFileBinary)
local savedXml = roblox.deserializeModel(savedFileXml)
assert(savedBinary[1].Name ~= "ROOT") assert(savedBinary[1].Name ~= "ROOT")
assert(savedXml[1].Name ~= "ROOT") assert(savedXml[1].Name ~= "ROOT")

View file

@ -0,0 +1,31 @@
local fs = require("@lune/fs")
local roblox = require("@lune/roblox") :: any
local Instance = roblox.Instance
local game = Instance.new("DataModel")
local workspace = game:GetService("Workspace")
local model = Instance.new("Model")
local part = Instance.new("Part")
part.Parent = model
model.Parent = workspace
local placeAsBinary = roblox.serializePlace(game)
local placeAsXml = roblox.serializePlace(game, true)
fs.writeFile("bin/temp-place.rbxl", placeAsBinary)
fs.writeFile("bin/temp-place.rbxlx", placeAsXml)
local savedFileBinary = fs.readFile("bin/temp-place.rbxl")
local savedFileXml = fs.readFile("bin/temp-place.rbxlx")
local savedBinary = roblox.deserializePlace(savedFileBinary)
local savedXml = roblox.deserializePlace(savedFileXml)
assert(savedBinary.Name ~= "ROOT")
assert(savedXml.Name ~= "ROOT")
assert(savedBinary.ClassName == "DataModel")
assert(savedXml.ClassName == "DataModel")

View file

@ -1,24 +0,0 @@
local roblox = require("@lune/roblox") :: any
local Instance = roblox.Instance
local game = Instance.new("DataModel")
local workspace = game:GetService("Workspace")
local model = Instance.new("Model")
local part = Instance.new("Part")
part.Parent = model
model.Parent = workspace
roblox.writePlaceFile("bin/temp-place.rbxl", game)
roblox.writePlaceFile("bin/temp-place.rbxlx", game)
local savedBinary = roblox.readPlaceFile("bin/temp-place.rbxl")
local savedXml = roblox.readPlaceFile("bin/temp-place.rbxlx")
assert(savedBinary.Name ~= "ROOT")
assert(savedXml.Name ~= "ROOT")
assert(savedBinary.ClassName == "DataModel")
assert(savedXml.ClassName == "DataModel")

View file

@ -1,4 +1,4 @@
local fs = require("@lune/fs") :: any local fs = require("@lune/fs")
local roblox = require("@lune/roblox") :: any local roblox = require("@lune/roblox") :: any
local BrickColor = roblox.BrickColor local BrickColor = roblox.BrickColor
@ -15,7 +15,8 @@ local Vector2 = roblox.Vector2
local Vector3 = roblox.Vector3 local Vector3 = roblox.Vector3
local Instance = roblox.Instance local Instance = roblox.Instance
local model = roblox.readModelFile("tests/roblox/rbx-test-files/models/attributes/binary.rbxm")[1] local modelFile = fs.readFile("tests/roblox/rbx-test-files/models/attributes/binary.rbxm")
local model = roblox.deserializeModel(modelFile)[1]
model:SetAttribute("Foo", "Bar") model:SetAttribute("Foo", "Bar")
@ -99,5 +100,6 @@ assert(folder:GetAttribute("Foo") == "Bar")
local game = Instance.new("DataModel") local game = Instance.new("DataModel")
model.Parent = game model.Parent = game
local placeFile = roblox.serializePlace(game)
fs.writeDir("bin/roblox") fs.writeDir("bin/roblox")
roblox.writePlaceFile("bin/roblox/attributes.rbxl", game) fs.writeFile("bin/roblox/attributes.rbxl", placeFile)

View file

@ -1,8 +1,9 @@
local fs = require("@lune/fs")
local roblox = require("@lune/roblox") :: any local roblox = require("@lune/roblox") :: any
local Instance = roblox.Instance local Instance = roblox.Instance
local model = local modelFile = fs.readFile("tests/roblox/rbx-test-files/models/three-nested-folders/binary.rbxm")
roblox.readModelFile("tests/roblox/rbx-test-files/models/three-nested-folders/binary.rbxm")[1] local model = roblox.deserializeModel(modelFile)[1]
assert(#model:GetChildren() == 1) assert(#model:GetChildren() == 1)

View file

@ -1,8 +1,9 @@
local fs = require("@lune/fs")
local roblox = require("@lune/roblox") :: any local roblox = require("@lune/roblox") :: any
local Instance = roblox.Instance local Instance = roblox.Instance
local model = local modelFile = fs.readFile("tests/roblox/rbx-test-files/models/three-nested-folders/binary.rbxm")
roblox.readModelFile("tests/roblox/rbx-test-files/models/three-nested-folders/binary.rbxm")[1] local model = roblox.deserializeModel(modelFile)[1]
assert(#model:GetDescendants() == 2) assert(#model:GetDescendants() == 2)

View file

@ -1,7 +1,8 @@
local fs = require("@lune/fs")
local roblox = require("@lune/roblox") :: any local roblox = require("@lune/roblox") :: any
local model = local modelFile = fs.readFile("tests/roblox/rbx-test-files/models/three-nested-folders/binary.rbxm")
roblox.readModelFile("tests/roblox/rbx-test-files/models/three-nested-folders/binary.rbxm")[1] local model = roblox.deserializeModel(modelFile)[1]
local child = model:FindFirstChild("Parent") local child = model:FindFirstChild("Parent")
local descendant = child:FindFirstChild("Child") local descendant = child:FindFirstChild("Child")