diff --git a/CHANGELOG.md b/CHANGELOG.md index 92e1415..9666564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ * **Breaking:** `Instance.new` now only works for instances that actually exist. * Added `Instance:Clone()` for copying instances all over the place, as is Roblox tradition. ([#12](https://github.com/rojo-rbx/remodel/issues/12)) * Added `DataModel:GetService()` for finding services and creating them if they don't exist, like Roblox does. ([#10](https://github.com/rojo-rbx/remodel/issues/10)) -* Added `remodel.getRawProperty(instance, name)` for an initial stab at reading property values. +* Added `remodel.getRawProperty(instance, name)`, a clunky but powerful API for reading properties with no validation. +* Added `remodel.setRawProperty(instance, name, type, value)` for writing properties with no validation. * Fixed Remodel dropping unknown properties when reading/writing XML models. This should make Remodel's behavior line up with Rojo. * Improved error messages in preparation for [#7](https://github.com/rojo-rbx/remodel/issues/7) to be fixed upstream. diff --git a/README.md b/README.md index e8cf749..f64d4fa 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,33 @@ If the instance is a `DataModel`, this method will throw. Places should be uploa Throws on error. +### `remodel.getRawProperty` (**unreleased**) +``` +remodel.getRawProperty(instance: Instance, name: string): any? +``` + +Gets the property with the given name from the given instance, bypassing all validation. + +This is intended to be a simple to implement but very powerful API while Remodel grows more ergonomic functionality. + +Throws if the value type stored on the instance cannot be represented by Remodel yet. See [Supported Roblox Types](#supported-roblox-types) for more details. + +### `remodel.setRawProperty` (**unreleased**) +``` +remodel.setRawProperty( + instance: Instance, + name: string, + type: string, + value: any, +) +``` + +Sets a property on the given instance with the name, type, and value given. Valid values for `type` are defined in [Supported Roblox Types](#supported-roblox-types) in the left half of the bulleted list. + +This is intended to be a simple to implement but very powerful API while Remodel grows more ergonomic functionality. + +Throws if the value type cannot be represented by Remodel yet. See [Supported Roblox Types](#supported-roblox-types) for more details. + ### `remodel.readFile` (0.3.0+) ``` remodel.readFile(path: string): string @@ -202,6 +229,21 @@ This is a thin wrapper around Rust's [`fs::create_dir_all`](https://doc.rust-lan Throws on error. +## Supported Roblox Types +When interacting with Roblox instances, Remodel doesn't support all value types yet and may throw an error. + +Supported types and their Lua equivalents: + +* `String`: `string` +* `Content`: `string` +* `Bool`: `boolean` +* `Float64`: `number` +* `Float32`: `number` +* `Int64`: `number` +* `Int32`: `number` + +More types will be added as time goes on, and Remodel will slowly begin to automatically infer correct types in more contexts. + ## Authentication Some of Remodel's APIs access the Roblox web API and need authentication in the form of a `.ROBLOSECURITY` cookie to access private assets. Auth cookies look like this: diff --git a/src/main.rs b/src/main.rs index 56a1628..27346fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod auth_cookie; mod remodel_api; mod remodel_context; mod roblox_api; +mod value; use std::{ error::Error, diff --git a/src/remodel_api/remodel.rs b/src/remodel_api/remodel.rs index f16c989..5bf0986 100644 --- a/src/remodel_api/remodel.rs +++ b/src/remodel_api/remodel.rs @@ -7,12 +7,15 @@ use std::{ sync::Arc, }; -use rbx_dom_weak::{RbxInstanceProperties, RbxTree, RbxValue}; +use rbx_dom_weak::{RbxInstanceProperties, RbxTree, RbxValueType}; use reqwest::header::{CONTENT_TYPE, COOKIE, USER_AGENT}; -use rlua::{Context, ToLua, UserData, UserDataMethods}; +use rlua::{Context, UserData, UserDataMethods}; use super::LuaInstance; -use crate::remodel_context::RemodelContext; +use crate::{ + remodel_context::RemodelContext, + value::{lua_to_rbxvalue, rbxvalue_to_lua, type_from_str}, +}; fn xml_encode_options() -> rbx_xml::EncodeOptions { rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown) @@ -306,17 +309,32 @@ impl Remodel { let tree = lua_instance.tree.lock().unwrap(); let instance = tree.get_instance(lua_instance.id).ok_or_else(|| { - rlua::Error::external("Cannot call remodel.GetRawProperty on a destroyed instance.") + rlua::Error::external("Cannot call remodel.getRawProperty on a destroyed instance.") })?; match instance.properties.get(name) { - Some(value) => match value { - RbxValue::String { value } => value.as_str().to_lua(context), - _ => unimplemented!(), - }, + Some(value) => rbxvalue_to_lua(context, value), None => Ok(rlua::Value::Nil), } } + + fn set_raw_property( + lua_instance: LuaInstance, + key: String, + ty: RbxValueType, + lua_value: rlua::Value<'_>, + ) -> rlua::Result<()> { + let mut tree = lua_instance.tree.lock().unwrap(); + + let instance = tree.get_instance_mut(lua_instance.id).ok_or_else(|| { + rlua::Error::external("Cannot call remodel.setRawProperty on a destroyed instance.") + })?; + + let value = lua_to_rbxvalue(ty, lua_value)?; + instance.properties.insert(key, value); + + Ok(()) + } } impl UserData for Remodel { @@ -328,6 +346,16 @@ impl UserData for Remodel { }, ); + methods.add_function( + "setRawProperty", + |_context, (instance, name, lua_ty, value): (LuaInstance, String, String, rlua::Value<'_>)| { + let ty = type_from_str(&lua_ty) + .ok_or_else(|| rlua::Error::external(format!("{} is not a valid Roblox type.", lua_ty)))?; + + Self::set_raw_property(instance, name, ty, value) + }, + ); + methods.add_function("readPlaceFile", |context, lua_path: String| { let path = Path::new(&lua_path); diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..ae874df --- /dev/null +++ b/src/value.rs @@ -0,0 +1,134 @@ +//! Defines how to turn RbxValue values into Lua values and back. + +use rbx_dom_weak::{RbxValue, RbxValueType}; +use rlua::{Context, Result as LuaResult, ToLua, Value as LuaValue}; + +pub fn rbxvalue_to_lua<'lua>( + context: Context<'lua>, + value: &RbxValue, +) -> LuaResult> { + use RbxValue::*; + + fn unimplemented_type(name: &str) -> LuaResult> { + Err(rlua::Error::external(format!( + "Values of type {} are not yet implemented.", + name + ))) + } + + match value { + BinaryString { value: _ } => unimplemented_type("BinaryString"), + BrickColor { value: _ } => unimplemented_type("BrickColor"), + Bool { value } => value.to_lua(context), + CFrame { value: _ } => unimplemented_type("CFrame"), + Color3 { value: _ } => unimplemented_type("Color3"), + Color3uint8 { value: _ } => unimplemented_type("Color3uint8"), + ColorSequence { value: _ } => unimplemented_type("ColorSequence"), + Content { value } => value.as_str().to_lua(context), + Enum { value: _ } => unimplemented_type("Enum"), + Float32 { value } => value.to_lua(context), + Float64 { value } => value.to_lua(context), + Int32 { value } => value.to_lua(context), + Int64 { value } => value.to_lua(context), + NumberRange { value: _ } => unimplemented_type("NumberRange"), + NumberSequence { value: _ } => unimplemented_type("NumberSequence"), + PhysicalProperties { value: _ } => unimplemented_type("PhysicalProperties"), + Ray { value: _ } => unimplemented_type("Ray"), + Rect { value: _ } => unimplemented_type("Rect"), + Ref { value: _ } => unimplemented_type("Ref"), + SharedString { value: _ } => unimplemented_type("SharedString"), + String { value } => value.as_str().to_lua(context), + UDim { value: _ } => unimplemented_type("UDim"), + UDim2 { value: _ } => unimplemented_type("UDim2"), + Vector2 { value: _ } => unimplemented_type("Vector2"), + Vector2int16 { value: _ } => unimplemented_type("Vector2int16"), + Vector3 { value: _ } => unimplemented_type("Vector3"), + Vector3int16 { value: _ } => unimplemented_type("Vector3int16"), + + _ => Err(rlua::Error::external(format!( + "The type '{:?}' is unknown to Remodel, please file a bug!", + value.get_type() + ))), + } +} + +pub fn lua_to_rbxvalue(ty: RbxValueType, value: LuaValue<'_>) -> LuaResult { + match (ty, value) { + (RbxValueType::String, LuaValue::String(lua_string)) => Ok(RbxValue::String { + value: lua_string.to_str()?.to_owned(), + }), + (RbxValueType::Content, LuaValue::String(lua_string)) => Ok(RbxValue::String { + value: lua_string.to_str()?.to_owned(), + }), + + (RbxValueType::Bool, LuaValue::Boolean(value)) => Ok(RbxValue::Bool { value }), + + (RbxValueType::Float32, LuaValue::Number(value)) => Ok(RbxValue::Float32 { + value: value as f32, + }), + (RbxValueType::Float32, LuaValue::Integer(value)) => Ok(RbxValue::Float32 { + value: value as f32, + }), + + (RbxValueType::Float64, LuaValue::Number(value)) => Ok(RbxValue::Float64 { + value: value as f64, + }), + (RbxValueType::Float64, LuaValue::Integer(value)) => Ok(RbxValue::Float64 { + value: value as f64, + }), + + (RbxValueType::Int32, LuaValue::Number(value)) => Ok(RbxValue::Int32 { + value: value as i32, + }), + (RbxValueType::Int32, LuaValue::Integer(value)) => Ok(RbxValue::Int32 { + value: value as i32, + }), + + (RbxValueType::Int64, LuaValue::Number(value)) => Ok(RbxValue::Int64 { + value: value as i64, + }), + (RbxValueType::Int64, LuaValue::Integer(value)) => Ok(RbxValue::Int64 { + value: value as i64, + }), + + (_, unknown_value) => Err(rlua::Error::external(format!( + "The Lua value {:?} could not be converted to the Roblox type {:?}", + unknown_value, ty + ))), + } +} + +pub fn type_from_str(name: &str) -> Option { + use RbxValueType::*; + + match name { + "BinaryString" => Some(BinaryString), + "BrickColor" => Some(BrickColor), + "Bool" => Some(Bool), + "CFrame" => Some(CFrame), + "Color3" => Some(Color3), + "Color3uint8" => Some(Color3uint8), + "ColorSequence" => Some(ColorSequence), + "Content" => Some(Content), + "Enum" => Some(Enum), + "Float32" => Some(Float32), + "Float64" => Some(Float64), + "Int32" => Some(Int32), + "Int64" => Some(Int64), + "NumberRange" => Some(NumberRange), + "NumberSequence" => Some(NumberSequence), + "PhysicalProperties" => Some(PhysicalProperties), + "Ray" => Some(Ray), + "Rect" => Some(Rect), + "Ref" => Some(Ref), + "SharedString" => Some(SharedString), + "String" => Some(String), + "UDim" => Some(UDim), + "UDim2" => Some(UDim2), + "Vector2" => Some(Vector2), + "Vector2int16" => Some(Vector2int16), + "Vector3" => Some(Vector3), + "Vector3int16" => Some(Vector3int16), + _ => None, + } +} diff --git a/test-scripts/raw-property-bad-conversion.lua b/test-scripts/raw-property-bad-conversion.lua new file mode 100644 index 0000000..bf81bde --- /dev/null +++ b/test-scripts/raw-property-bad-conversion.lua @@ -0,0 +1,7 @@ +local folder = Instance.new("Folder") + +local ok = pcall(function() + remodel.setRawProperty(folder, "PROPERTY_NAME", "Float64", "hello") +end) + +assert(not ok) \ No newline at end of file diff --git a/test-scripts/raw-property-new.lua b/test-scripts/raw-property-new.lua new file mode 100644 index 0000000..7acc981 --- /dev/null +++ b/test-scripts/raw-property-new.lua @@ -0,0 +1,5 @@ +local value = Instance.new("NumberValue") +remodel.setRawProperty(value, "Value", "Float64", 32) + +local value = remodel.getRawProperty(value, "Value") +assert(value == 32) \ No newline at end of file diff --git a/test-scripts/raw-property-number.lua b/test-scripts/raw-property-number.lua new file mode 100644 index 0000000..9234def --- /dev/null +++ b/test-scripts/raw-property-number.lua @@ -0,0 +1,11 @@ +local model = remodel.readModelFile("test-models/folder-and-value.rbxmx")[1] + +local numberValue = model.Number +assert(numberValue.ClassName == "NumberValue") + +local value = remodel.getRawProperty(numberValue, "Value") +assert(value == 42) + +remodel.setRawProperty(numberValue, "Value", "Float64", 8) +local value = remodel.getRawProperty(numberValue, "Value") +assert(value == 8) \ No newline at end of file diff --git a/test-scripts/get-raw-property.lua b/test-scripts/raw-property-string.lua similarity index 53% rename from test-scripts/get-raw-property.lua rename to test-scripts/raw-property-string.lua index c69f165..c0d02a0 100644 --- a/test-scripts/get-raw-property.lua +++ b/test-scripts/raw-property-string.lua @@ -4,4 +4,8 @@ local stringValue = model.String assert(stringValue.ClassName == "StringValue") local value = remodel.getRawProperty(stringValue, "Value") -assert(value == "Hello") \ No newline at end of file +assert(value == "Hello") + +remodel.setRawProperty(stringValue, "Value", "String", "Hello, world!") +local value = remodel.getRawProperty(stringValue, "Value") +assert(value == "Hello, world!") \ No newline at end of file