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
### 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 `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)
- 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
- Update to Luau version `0.576`

View file

@ -9,17 +9,22 @@ export type Instance = {}
### Example usage
```lua
local fs = require("@lune/fs")
local roblox = require("@lune/roblox")
-- Reading & writing a place file
local game = roblox.readPlaceFile("myPlaceFile.rbxl")
local workspace = game:GetService("Workspace")
-- Reading a place file
local placeFile = fs.readFile("myPlaceFile.rbxl")
local game = roblox.deserializePlace(placeFile)
-- Manipulating and reading instances - just like in Roblox!
local workspace = game:GetService("Workspace")
for _, child in workspace:GetChildren() do
print("Found child " .. child.Name .. " of class " .. child.ClassName)
end
roblox.writePlaceFile("myPlaceFile.rbxl", game)
-- Writing a place file
local newPlaceFile = roblox.serializePlace(game)
fs.writeFile("myPlaceFile.rbxl", newPlaceFile)
```
]=]
return {
@ -27,70 +32,100 @@ return {
@within Roblox
@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
```lua
local fs = require("@lune/fs")
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
end,
--[=[
@within Roblox
@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
```lua
local fs = require("@lune/fs")
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
end,
--[=[
@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
```lua
local fs = require("@lune/fs")
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 to write to the file
@param dataModel The DataModel for the place to serialize
@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
@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
```lua
local fs = require("@lune/fs")
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 write to the file
@param instances The array of instances to serialize
@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
@must_use

View file

@ -1,8 +1,5 @@
use std::path::PathBuf;
use blocking::unblock;
use mlua::prelude::*;
use tokio::fs;
use lune_roblox::{
document::{Document, DocumentError, DocumentFormat, DocumentKind},
@ -19,36 +16,19 @@ pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
}
TableBuilder::new(lua)?
.with_values(roblox_constants)?
.with_async_function("readPlaceFile", read_place_file)?
.with_async_function("readModelFile", read_model_file)?
.with_async_function("writePlaceFile", write_place_file)?
.with_async_function("writeModelFile", write_model_file)?
.with_async_function("deserializePlace", deserialize_place)?
.with_async_function("deserializeModel", deserialize_model)?
.with_async_function("serializePlace", serialize_place)?
.with_async_function("serializeModel", serialize_model)?
.with_async_function("getAuthCookie", get_auth_cookie)?
.build_readonly()
}
fn parse_file_path(path: String) -> LuaResult<(PathBuf, DocumentFormat)> {
let file_path = PathBuf::from(path);
let file_ext = file_path
.extension()
.ok_or_else(|| {
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)?;
async fn deserialize_place<'lua>(
lua: &'lua Lua,
contents: LuaString<'lua>,
) -> LuaResult<LuaValue<'lua>> {
let bytes = contents.as_bytes().to_vec();
let fut = unblock(move || {
let doc = Document::from_bytes(bytes, DocumentKind::Place)?;
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)
}
async fn read_model_file(lua: &Lua, path: String) -> LuaResult<LuaValue> {
let bytes = fs::read(path).await.map_err(LuaError::external)?;
async fn deserialize_model<'lua>(
lua: &'lua Lua,
contents: LuaString<'lua>,
) -> LuaResult<LuaValue<'lua>> {
let bytes = contents.as_bytes().to_vec();
let fut = unblock(move || {
let doc = Document::from_bytes(bytes, DocumentKind::Model)?;
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)
}
async fn write_place_file(_: &Lua, (path, data_model): (String, Instance)) -> LuaResult<()> {
let (file_path, doc_format) = parse_file_path(path)?;
async fn serialize_place(
lua: &Lua,
(data_model, as_xml): (Instance, Option<bool>),
) -> LuaResult<LuaString> {
let fut = unblock(move || {
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)
});
let bytes = fut.await?;
fs::write(file_path, bytes)
.await
.map_err(LuaError::external)?;
Ok(())
lua.create_string(&bytes)
}
async fn write_model_file(_: &Lua, (path, instances): (String, Vec<Instance>)) -> LuaResult<()> {
let (file_path, doc_format) = parse_file_path(path)?;
async fn serialize_model(
lua: &Lua,
(instances, as_xml): (Vec<Instance>, Option<bool>),
) -> LuaResult<LuaString> {
let fut = unblock(move || {
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)
});
let bytes = fut.await?;
fs::write(file_path, bytes)
.await
.map_err(LuaError::external)?;
Ok(())
lua.create_string(&bytes)
}
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_vector3int16: "roblox/datatypes/Vector3int16",
roblox_files_read_model: "roblox/files/readModelFile",
roblox_files_read_place: "roblox/files/readPlaceFile",
roblox_files_write_model: "roblox/files/writeModelFile",
roblox_files_write_place: "roblox/files/writePlaceFile",
roblox_files_deserialize_model: "roblox/files/deserializeModel",
roblox_files_deserialize_place: "roblox/files/deserializePlace",
roblox_files_serialize_model: "roblox/files/serializeModel",
roblox_files_serialize_place: "roblox/files/serializePlace",
roblox_instance_attributes: "roblox/instance/attributes",
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
for _, modelDir in modelDirs do
local modelBinary = roblox.readModelFile(modelDir .. "/binary.rbxl")
local modelXml = roblox.readModelFile(modelDir .. "/xml.rbxlx")
local modelFileBinary = fs.readFile(modelDir .. "/binary.rbxl")
local modelFileXml = fs.readFile(modelDir .. "/xml.rbxlx")
local modelBinary = roblox.deserializeModel(modelFileBinary)
local modelXml = roblox.deserializeModel(modelFileXml)
for _, modelInstance in modelBinary do
assert(modelInstance:IsA("Instance"))

View file

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

View file

@ -1,3 +1,4 @@
local fs = require("@lune/fs")
local roblox = require("@lune/roblox") :: any
local Instance = roblox.Instance
@ -6,11 +7,17 @@ local instances = {
Instance.new("Part"),
}
roblox.writeModelFile("bin/temp-model.rbxm", instances)
roblox.writeModelFile("bin/temp-model.rbxmx", instances)
local modelAsBinary = roblox.serializeModel(instances)
local modelAsXml = roblox.serializeModel(instances, true)
local savedBinary = roblox.readModelFile("bin/temp-model.rbxm")
local savedXml = roblox.readModelFile("bin/temp-model.rbxmx")
fs.writeFile("bin/temp-model.rbxm", modelAsBinary)
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(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 BrickColor = roblox.BrickColor
@ -15,7 +15,8 @@ local Vector2 = roblox.Vector2
local Vector3 = roblox.Vector3
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")
@ -99,5 +100,6 @@ assert(folder:GetAttribute("Foo") == "Bar")
local game = Instance.new("DataModel")
model.Parent = game
local placeFile = roblox.serializePlace(game)
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 Instance = roblox.Instance
local model =
roblox.readModelFile("tests/roblox/rbx-test-files/models/three-nested-folders/binary.rbxm")[1]
local modelFile = fs.readFile("tests/roblox/rbx-test-files/models/three-nested-folders/binary.rbxm")
local model = roblox.deserializeModel(modelFile)[1]
assert(#model:GetChildren() == 1)

View file

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

View file

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