diff --git a/CHANGELOG.md b/CHANGELOG.md index 39fe2b7..40008e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.
+ 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.
- Use the second argument of the already existing find methods instead to find descendants. - ### Changed - Update to Luau version `0.576` diff --git a/docs/typedefs/Roblox.luau b/docs/typedefs/Roblox.luau index aa392b4..7e0bc9c 100644 --- a/docs/typedefs/Roblox.luau +++ b/docs/typedefs/Roblox.luau @@ -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 diff --git a/packages/lib/src/builtins/roblox.rs b/packages/lib/src/builtins/roblox.rs index 7c930bf..c4184fd 100644 --- a/packages/lib/src/builtins/roblox.rs +++ b/packages/lib/src/builtins/roblox.rs @@ -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 { } 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 { - let bytes = fs::read(path).await.map_err(LuaError::external)?; +async fn deserialize_place<'lua>( + lua: &'lua Lua, + contents: LuaString<'lua>, +) -> LuaResult> { + 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 { fut.await?.to_lua(lua) } -async fn read_model_file(lua: &Lua, path: String) -> LuaResult { - let bytes = fs::read(path).await.map_err(LuaError::external)?; +async fn deserialize_model<'lua>( + lua: &'lua Lua, + contents: LuaString<'lua>, +) -> LuaResult> { + 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 { 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), +) -> LuaResult { 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)) -> LuaResult<()> { - let (file_path, doc_format) = parse_file_path(path)?; +async fn serialize_model( + lua: &Lua, + (instances, as_xml): (Vec, Option), +) -> LuaResult { 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) -> LuaResult> { diff --git a/packages/lib/src/tests.rs b/packages/lib/src/tests.rs index 406a540..9355b8d 100644 --- a/packages/lib/src/tests.rs +++ b/packages/lib/src/tests.rs @@ -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", diff --git a/tests/roblox/files/readModelFile.luau b/tests/roblox/files/deserializeModel.luau similarity index 65% rename from tests/roblox/files/readModelFile.luau rename to tests/roblox/files/deserializeModel.luau index 807a462..7a965b5 100644 --- a/tests/roblox/files/readModelFile.luau +++ b/tests/roblox/files/deserializeModel.luau @@ -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")) diff --git a/tests/roblox/files/readPlaceFile.luau b/tests/roblox/files/deserializePlace.luau similarity index 65% rename from tests/roblox/files/readPlaceFile.luau rename to tests/roblox/files/deserializePlace.luau index 2b05365..02da733 100644 --- a/tests/roblox/files/readPlaceFile.luau +++ b/tests/roblox/files/deserializePlace.luau @@ -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") diff --git a/tests/roblox/files/writeModelFile.luau b/tests/roblox/files/serializeModel.luau similarity index 50% rename from tests/roblox/files/writeModelFile.luau rename to tests/roblox/files/serializeModel.luau index 79bce09..3c02d79 100644 --- a/tests/roblox/files/writeModelFile.luau +++ b/tests/roblox/files/serializeModel.luau @@ -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") diff --git a/tests/roblox/files/serializePlace.luau b/tests/roblox/files/serializePlace.luau new file mode 100644 index 0000000..dd3d51d --- /dev/null +++ b/tests/roblox/files/serializePlace.luau @@ -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") diff --git a/tests/roblox/files/writePlaceFile.luau b/tests/roblox/files/writePlaceFile.luau deleted file mode 100644 index 7ff9a38..0000000 --- a/tests/roblox/files/writePlaceFile.luau +++ /dev/null @@ -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") diff --git a/tests/roblox/instance/attributes.luau b/tests/roblox/instance/attributes.luau index 926bb34..716e1ae 100644 --- a/tests/roblox/instance/attributes.luau +++ b/tests/roblox/instance/attributes.luau @@ -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) diff --git a/tests/roblox/instance/methods/GetChildren.luau b/tests/roblox/instance/methods/GetChildren.luau index 37952b6..dc6f824 100644 --- a/tests/roblox/instance/methods/GetChildren.luau +++ b/tests/roblox/instance/methods/GetChildren.luau @@ -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) diff --git a/tests/roblox/instance/methods/GetDescendants.luau b/tests/roblox/instance/methods/GetDescendants.luau index 7e6c6df..58c0cf4 100644 --- a/tests/roblox/instance/methods/GetDescendants.luau +++ b/tests/roblox/instance/methods/GetDescendants.luau @@ -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) diff --git a/tests/roblox/instance/methods/GetFullName.luau b/tests/roblox/instance/methods/GetFullName.luau index d097148..f249e77 100644 --- a/tests/roblox/instance/methods/GetFullName.luau +++ b/tests/roblox/instance/methods/GetFullName.luau @@ -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")