Compare commits

..

9 Commits

Author SHA1 Message Date
Lucien Greathouse
eddc469f95 Release 6.2.0 2021-06-10 00:45:10 -04:00
Lucien Greathouse
21a4667fe4 Fix failing snapshot 2021-06-10 00:44:53 -04:00
Lucien Greathouse
b25f2fcd5d Update dependencies 2021-06-10 00:41:35 -04:00
Lucien Greathouse
0f7c9493d2 Fix 'Open Scripts Externally' crashing studio.
Closes #369.
2021-04-23 17:08:11 -04:00
Lucien Greathouse
f1c4102d7f Update changelog 2021-04-23 16:00:45 -04:00
Lucien Greathouse
8b5bfd5f44 Mark two-way sync as experimental in UI 2021-04-23 15:59:31 -04:00
Lucien Greathouse
0599b50235 Release 6.1.0 2021-04-12 17:19:35 -04:00
Lucien Greathouse
21f7ef6186 Update dependencies 2021-04-09 18:44:03 -04:00
MSAA
de6470bb45 change server bind address (#403)
* web/mod.rs - change server bind address

127.0.0.1 is a loopback interface, and only works on the same host
0.0.0.0 will allow connections from other hosts

ideally, this should be a console arg - but it's a quick fix

* implement --address option, revert default bind address to 127.0.0.1

* revert silly autoformatting

* ok, actually using rustfmt now

* More precise --address flag description

* Use SocketAddr where available, take advantage of const-ness

* Display 'localhost' if address is loopback

* Update Changelog

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2021-03-31 16:44:10 -04:00
142 changed files with 22308 additions and 49291 deletions

View File

@@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
rust_version: [stable, "1.46.0"]
rust_version: [stable, "1.43.1"]
steps:
- uses: actions/checkout@v1

View File

@@ -2,102 +2,21 @@
## Unreleased Changes
## [7.0.0-rc.1][7.0.0-rc.1] (August 23, 2021)
In Rojo 6 and previous Rojo 7 alphas, an explicit Vector3 property would be written like this:
```json
{
"className": "Part",
"properties": {
"Position": {
"Type": "Vector3",
"Value": [1, 2, 3]
}
}
}
```
For Rojo 7, this will need to be changed to:
```json
{
"className": "Part",
"properties": {
"Position": {
"Vector3": [1, 2, 3]
}
}
}
```
The shorthand property format that most users use is not impacted. For reference, it looks like this:
```json
{
"className": "Part",
"properties": {
"Position": [1, 2, 3]
}
}
```
* Major breaking change: changed property syntax for project files; shorthand syntax is unchanged.
* Added the `fmt-project` subcommand for formatting Rojo project files.
* Improved error output for many subcommands.
* Updated to stable versions of rbx-dom libraries.
* Updated async infrastructure, which should fix a handful of bugs. ([#459][#459])
* Fixed syncing refs in the Roblox Studio plugin ([#462][#462], [#466][#466])
* Added support for long paths on Windows. ([#464][#464])
[#459]: https://github.com/rojo-rbx/rojo/pull/459
[#462]: https://github.com/rojo-rbx/rojo/pull/462
[#464]: https://github.com/rojo-rbx/rojo/pull/464
[#466]: https://github.com/rojo-rbx/rojo/pull/466
[7.0.0-rc.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.0.0-rc.1
## [7.0.0-alpha.4][7.0.0-alpha.4] (May 5, 2021)
* Added the `gameId` and `placeId` optional properties to project files.
* When connecting from the Rojo Roblox Studio plugin, Rojo will set the game and place ID of the current place to these values, if set.
* This is equivalent to running `game:SetUniverseId(...)` and `game:SetPlaceId(...)` from the command bar in Studio.
## [6.2.0][6.2.0] (June 10, 2021)
* Added "EXPERIMENTAL!" label to two-way sync toggle in Rojo's Roblox Studio plugin.
* Fixed `Name` and `Parent` properties being allowed in Rojo projects. ([#413][pr-413])
* Fixed "Open Scripts Externally" feature crashing Studio. ([#369][issue-369])
* Empty `.model.json` files will no longer cause errors. ([#420][pr-420])
* When specifying `$path` on a service, Rojo now keeps the correct class name. ([#331][issue-331])
* Improved error messages for misconfigured projects.
* Fixed "Open Scripts Externally" feature crashing Studio ([#369][issue-369])
* Updated dependencies, fixing `HumanoidDescription` ID issues.
[issue-331]: https://github.com/rojo-rbx/rojo/issues/331
[issue-369]: https://github.com/rojo-rbx/rojo/issues/369
[pr-420]: https://github.com/rojo-rbx/rojo/pull/420
[pr-413]: https://github.com/rojo-rbx/rojo/pull/413
[7.0.0-alpha.4]: https://github.com/rojo-rbx/rojo/releases/tag/v7.0.0-alpha.4
[6.2.0]: https://github.com/rojo-rbx/rojo/releases/tag/v6.2.0
## [6.1.0][6.1.0] (April 12, 2021)
* Updated dependencies, fixing OptionalCoordinateFrame-related issues.
## [7.0.0-alpha.3][7.0.0-alpha.3] (February 19, 2021)
* Updated dependencies, fixing `OptionalCoordinateFrame`-related issues.
* Added `--address` flag to `rojo serve` to allow for external connections. ([#403][pr-403])
[pr-403]: https://github.com/rojo-rbx/rojo/pull/403
[7.0.0-alpha.3]: https://github.com/rojo-rbx/rojo/releases/tag/v7.0.0-alpha.3
## [7.0.0-alpha.2][7.0.0-alpha.2] (February 19, 2021)
* Fixed incorrect protocol version between the client and server.
[7.0.0-alpha.2]: https://github.com/rojo-rbx/rojo/releases/tag/v7.0.0-alpha.2
## [7.0.0-alpha.1][7.0.0-alpha.1] (February 18, 2021)
This release includes a brand new implementation of the Roblox DOM. It brings performance improvements, much better support for `rbxl` and `rbxm` files, and a better internal API.
* Added support for all remaining property types.
* Added support for the entire Roblox binary model format.
* Changed `rojo upload` to upload binary places and models instead of XML.
* This should make using `rojo upload` much more feasible for large places.
* **Breaking**: Changed format of some types of values in `project.json`, `model.json`, and `meta.json` files.
* This should impact few projects. See [this file][allValues.json] for new examples of each property type.
Formatting of types will change more before the stable release of Rojo 7. We're hoping to use this opportunity to normalize some of the case inconsistency introduced in Rojo 0.5.
[7.0.0-alpha.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.0.0-alpha.1
[allValues.json]: https://github.com/rojo-rbx/rojo/blob/f4a790eb50b74e482000bad1dcfe22533992fb20/plugin/rbx_dom_lua/src/allValues.json
[6.1.0]: https://github.com/rojo-rbx/rojo/releases/tag/v6.1.0
## [6.0.2](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.2) (February 9, 2021)
* Fixed `rojo upload` to handle CSRF challenges.

View File

@@ -41,9 +41,9 @@ The Rojo release process is pretty manual right now. If you need to do it, here'
6. Tag the commit with the version from `Cargo.toml` prepended with a v, like `v0.4.13`
7. Publish the CLI
* `cargo publish`
8. Publish the Plugin
* `rojo publish plugin --asset_id 6415005344`
8. Build and upload the plugin
* `rojo build plugin -o Rojo.rbxm`
* Upload `Rojo.rbxm` to Roblox.com, keep it for later
9. Push commits and tags
* `git push && git push --tags`
10. Copy GitHub release content from previous release

1102
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "rojo"
version = "7.0.0-rc.1"
version = "6.2.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "Enables professional-grade development tools for Roblox developers"
license = "MPL-2.0"
@@ -9,7 +9,6 @@ documentation = "https://rojo.space/docs"
repository = "https://github.com/rojo-rbx/rojo"
readme = "README.md"
edition = "2018"
build = "build.rs"
exclude = [
"/test-projects/**",
@@ -46,58 +45,48 @@ name = "build"
harness = false
[dependencies]
memofs = { version = "0.2.0", path = "memofs" }
# These dependencies can be uncommented when working on rbx-dom simultaneously
# rbx_binary = { path = "../rbx-dom/rbx_binary" }
# rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" }
# rbx_reflection = { path = "../rbx-dom/rbx_reflection" }
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
rbx_binary = "0.6.1"
rbx_dom_weak = "2.1.0"
rbx_reflection = "4.1.0"
rbx_reflection_database = "0.2.1"
rbx_xml = "0.12.1"
memofs = { version = "0.1.2", path = "memofs" }
anyhow = "1.0.27"
backtrace = "0.3"
bincode = "1.2.1"
crossbeam-channel = "0.5.1"
crossbeam-channel = "0.4.0"
csv = "1.1.1"
env_logger = "0.9.0"
env_logger = "0.7.1"
fs-err = "2.2.0"
futures = "0.3.16"
futures = "0.1.29"
globset = "0.4.4"
humantime = "2.1.0"
hyper = { version = "0.14.11", features = ["server", "tcp", "http1"] }
humantime = "1.3.0"
hyper = "0.12.35"
jod-thread = "0.1.0"
lazy_static = "1.4.0"
log = "0.4.8"
maplit = "1.0.1"
notify = "4.0.14"
opener = "0.5.0"
opener = "0.4.1"
rbx_binary = "0.5.0"
rbx_dom_weak = "1.10.1"
rbx_reflection = "3.3.408"
rbx_xml = "0.11.3"
regex = "1.3.1"
reqwest = "0.9.20"
ritz = "0.1.0"
rlua = "0.17.0"
roblox_install = "1.0.0"
roblox_install = "0.2.2"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
structopt = "0.3.5"
termcolor = "1.0.5"
thiserror = "1.0.11"
tokio = { version = "1.9.0", features = ["rt", "rt-multi-thread"] }
tokio = "0.1.22"
uuid = { version = "0.8.1", features = ["v4", "serde"] }
[target.'cfg(windows)'.dependencies]
winreg = "0.9.0"
winreg = "0.6.2"
[build-dependencies]
memofs = { version = "0.2.0", path = "memofs" }
memofs = { version = "0.1.3", path = "memofs" }
embed-resource = "1.6"
anyhow = "1.0.27"
bincode = "1.2.1"
fs-err = "2.3.0"
@@ -109,8 +98,8 @@ rojo-insta-ext = { path = "rojo-insta-ext" }
criterion = "0.3"
insta = { version = "1.3.0", features = ["redactions"] }
lazy_static = "1.2"
paste = "1.0.5"
pretty_assertions = "0.7.2"
paste = "0.1"
pretty_assertions = "0.6.1"
serde_yaml = "0.8.9"
tempfile = "3.0"
walkdir = "2.1"

View File

@@ -1,13 +1,21 @@
<div align="center">
<a href="https://rojo.space"><img src="assets/logo-512.png" alt="Rojo" height="217" /></a>
<a href="https://rojo.space">
<img src="assets/logo-512.png" alt="Rojo" height="217" />
</a>
</div>
<div>&nbsp;</div>
<div align="center">
<a href="https://github.com/rojo-rbx/rojo/actions"><img src="https://github.com/rojo-rbx/rojo/workflows/CI/badge.svg" alt="Actions status" /></a>
<a href="https://crates.io/crates/rojo"><img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" /></a>
<a href="https://rojo.space/docs"><img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" /></a>
<a href="https://github.com/rojo-rbx/rojo/actions">
<img src="https://github.com/rojo-rbx/rojo/workflows/CI/badge.svg" alt="Actions status" />
</a>
<a href="https://crates.io/crates/rojo">
<img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" />
</a>
<a href="https://rojo.space/docs">
<img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" />
</a>
</div>
<hr />
@@ -40,7 +48,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
Pull requests are welcome!
Rojo supports Rust 1.46.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
Rojo supports Rust 1.43.1 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
## License
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.

View File

@@ -73,9 +73,5 @@ fn main() -> Result<(), anyhow::Error> {
bincode::serialize_into(out_file, &snapshot)?;
println!("cargo:rerun-if-changed=build/windows/rojo-manifest.rc");
println!("cargo:rerun-if-changed=build/windows/rojo.manifest");
embed_resource::compile("build/windows/rojo-manifest.rc");
Ok(())
}

View File

@@ -1,2 +0,0 @@
#define RT_MANIFEST 24
1 RT_MANIFEST "rojo.manifest"

View File

@@ -1,8 +0,0 @@
<?xml version='1.0' encoding='utf-8' standalone='yes'?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
<ws2:longPathAware>true</ws2:longPathAware>
</windowsSettings>
</application>
</assembly>

View File

@@ -1,3 +1,3 @@
[tools]
rojo = { source = "rojo-rbx/rojo", version = "6.1.0" }
rojo = { source = "rojo-rbx/rojo", version = "6.0.0-rc.3" }
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }

View File

@@ -2,9 +2,6 @@
## Unreleased Changes
## 0.2.0 (2021-08-23)
* Updated to `crossbeam-channel` 0.5.1.
## 0.1.3 (2020-11-19)
* Added `set_watch_enabled` to `Vfs` and `VfsLock` to allow turning off file watching.

View File

@@ -1,7 +1,7 @@
[package]
name = "memofs"
description = "Virtual filesystem with configurable backends."
version = "0.2.0"
version = "0.1.3"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
edition = "2018"
readme = "README.md"
@@ -11,7 +11,7 @@ homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
crossbeam-channel = "0.5.1"
crossbeam-channel = "0.4.0"
fs-err = "2.3.0"
notify = "4.0.15"
serde = { version = "1.0", features = ["derive"] }

View File

@@ -0,0 +1,44 @@
stds.roblox = {
read_globals = {
game = {
other_fields = true,
},
-- Roblox globals
"script",
-- Extra functions
"tick", "warn",
"wait", "typeof",
-- Types
"CFrame",
"Color3",
"Enum",
"Instance",
"NumberRange",
"Rect",
"UDim", "UDim2",
"Vector2", "Vector3",
"Vector2int16", "Vector3int16",
}
}
stds.testez = {
read_globals = {
"describe",
"it", "itFOCUS", "itSKIP",
"FOCUS", "SKIP", "HACK_NO_XPCALL",
"expect",
}
}
ignore = {
"212", -- unused arguments
}
std = "lua51+roblox"
files["**/*.spec.lua"] = {
std = "+testez",
}

View File

@@ -1,457 +0,0 @@
local base64 = require(script.Parent.base64)
local function identity(...)
return ...
end
local function unpackDecoder(f)
return function(value)
return f(unpack(value))
end
end
local function serializeFloat(value)
-- TODO: Figure out a better way to serialize infinity and NaN, neither of
-- which fit into JSON.
if value == math.huge or value == -math.huge then
return 999999999 * math.sign(value)
end
return value
end
local ALL_AXES = {"X", "Y", "Z"}
local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"}
local types
types = {
Axes = {
fromPod = function(pod)
local axes = {}
for index, axisName in ipairs(pod) do
axes[index] = Enum.Axis[axisName]
end
return Axes.new(unpack(axes))
end,
toPod = function(roblox)
local json = {}
for _, axis in ipairs(ALL_AXES) do
if roblox[axis] then
table.insert(json, axis)
end
end
return json
end,
},
BinaryString = {
fromPod = base64.decode,
toPod = base64.encode,
},
Bool = {
fromPod = identity,
toPod = identity,
},
BrickColor = {
fromPod = function(pod)
return BrickColor.new(pod)
end,
toPod = function(roblox)
return roblox.Number
end,
},
CFrame = {
fromPod = function(pod)
local pos = pod.position
local orient = pod.orientation
return CFrame.new(
pos[1], pos[2], pos[3],
orient[1][1], orient[1][2], orient[1][3],
orient[2][1], orient[2][2], orient[2][3],
orient[3][1], orient[3][2], orient[3][3]
)
end,
toPod = function(roblox)
local x, y, z,
r00, r01, r02,
r10, r11, r12,
r20, r21, r22 = roblox:GetComponents()
return {
position = {x, y, z},
orientation = {
{r00, r01, r02},
{r10, r11, r12},
{r20, r21, r22},
},
}
end,
},
Color3 = {
fromPod = unpackDecoder(Color3.new),
toPod = function(roblox)
return {roblox.r, roblox.g, roblox.b}
end,
},
Color3uint8 = {
fromPod = unpackDecoder(Color3.fromRGB),
toPod = function(roblox)
return {
math.round(roblox.R * 255),
math.round(roblox.G * 255),
math.round(roblox.B * 255),
}
end,
},
ColorSequence = {
fromPod = function(pod)
local keypoints = {}
for index, keypoint in ipairs(pod.keypoints) do
keypoints[index] = ColorSequenceKeypoint.new(
keypoint.time,
types.Color3.fromPod(keypoint.color)
)
end
return ColorSequence.new(keypoints)
end,
toPod = function(roblox)
local keypoints = {}
for index, keypoint in ipairs(roblox.Keypoints) do
keypoints[index] = {
time = keypoint.Time,
color = types.Color3.toPod(keypoint.Value),
}
end
return {
keypoints = keypoints,
}
end,
},
Content = {
fromPod = identity,
toPod = identity,
},
Enum = {
fromPod = identity,
toPod = function(roblox)
-- FIXME: More robust handling of enums
if typeof(roblox) == "number" then
return roblox
else
return roblox.Value
end
end,
},
Faces = {
fromPod = function(pod)
local faces = {}
for index, faceName in ipairs(pod) do
faces[index] = Enum.NormalId[faceName]
end
return Faces.new(unpack(faces))
end,
toPod = function(roblox)
local pod = {}
for _, face in ipairs(ALL_FACES) do
if roblox[face] then
table.insert(pod, face)
end
end
return pod
end,
},
Float32 = {
fromPod = identity,
toPod = serializeFloat,
},
Float64 = {
fromPod = identity,
toPod = serializeFloat,
},
Int32 = {
fromPod = identity,
toPod = identity,
},
Int64 = {
fromPod = identity,
toPod = identity,
},
NumberRange = {
fromPod = unpackDecoder(NumberRange.new),
toPod = function(roblox)
return {roblox.Min, roblox.Max}
end,
},
NumberSequence = {
fromPod = function(pod)
local keypoints = {}
for index, keypoint in ipairs(pod.keypoints) do
keypoints[index] = NumberSequenceKeypoint.new(
keypoint.time,
keypoint.value,
keypoint.envelope
)
end
return NumberSequence.new(keypoints)
end,
toPod = function(roblox)
local keypoints = {}
for index, keypoint in ipairs(roblox.Keypoints) do
keypoints[index] = {
time = keypoint.Time,
value = keypoint.Value,
envelope = keypoint.Envelope,
}
end
return {
keypoints = keypoints,
}
end,
},
PhysicalProperties = {
fromPod = function(pod)
if pod == "Default" then
return nil
else
return PhysicalProperties.new(
pod.density,
pod.friction,
pod.elasticity,
pod.frictionWeight,
pod.elasticityWeight
)
end
end,
toPod = function(roblox)
if roblox == nil then
return "Default"
else
return {
density = roblox.Density,
friction = roblox.Friction,
elasticity = roblox.Elasticity,
frictionWeight = roblox.FrictionWeight,
elasticityWeight = roblox.ElasticityWeight,
}
end
end,
},
Ray = {
fromPod = function(pod)
return Ray.new(
types.Vector3.fromPod(pod.origin),
types.Vector3.fromPod(pod.direction)
)
end,
toPod = function(roblox)
return {
origin = types.Vector3.toPod(roblox.Origin),
direction = types.Vector3.toPod(roblox.Direction),
}
end,
},
Rect = {
fromPod = function(pod)
return Rect.new(
types.Vector2.fromPod(pod[1]),
types.Vector2.fromPod(pod[2])
)
end,
toPod = function(roblox)
return {
types.Vector2.toPod(roblox.Min),
types.Vector2.toPod(roblox.Max),
}
end,
},
Ref = {
fromPod = function(_pod)
error("Ref cannot be decoded on its own")
end,
toPod = function(_roblox)
error("Ref can not be encoded on its own")
end,
},
Region3 = {
fromPod = function(pod)
error("Region3 is not implemented")
end,
toPod = function(roblox)
error("Region3 is not implemented")
end,
},
Region3int16 = {
fromPod = function(pod)
return Region3int16.new(
types.Vector3int16.fromPod(pod[1]),
types.Vector3int16.fromPod(pod[2])
)
end,
toPod = function(roblox)
return {
types.Vector3int16.toPod(roblox.Min),
types.Vector3int16.toPod(roblox.Max),
}
end,
},
SharedString = {
fromPod = function(pod)
error("SharedString is not supported")
end,
toPod = function(roblox)
error("SharedString is not supported")
end,
},
String = {
fromPod = identity,
toPod = identity,
},
UDim = {
fromPod = unpackDecoder(UDim.new),
toPod = function(roblox)
return {roblox.Scale, roblox.Offset}
end,
},
UDim2 = {
fromPod = function(pod)
return UDim2.new(
types.UDim.fromPod(pod[1]),
types.UDim.fromPod(pod[2])
)
end,
toPod = function(roblox)
return {
types.UDim.toPod(roblox.X),
types.UDim.toPod(roblox.Y),
}
end,
},
Vector2 = {
fromPod = unpackDecoder(Vector2.new),
toPod = function(roblox)
return {
serializeFloat(roblox.X),
serializeFloat(roblox.Y),
}
end,
},
Vector2int16 = {
fromPod = unpackDecoder(Vector2int16.new),
toPod = function(roblox)
return {roblox.X, roblox.Y}
end,
},
Vector3 = {
fromPod = unpackDecoder(Vector3.new),
toPod = function(roblox)
return {
serializeFloat(roblox.X),
serializeFloat(roblox.Y),
serializeFloat(roblox.Z),
}
end,
},
Vector3int16 = {
fromPod = unpackDecoder(Vector3int16.new),
toPod = function(roblox)
return {roblox.X, roblox.Y, roblox.Z}
end,
},
}
local EncodedValue = {}
function EncodedValue.decode(encodedValue)
local ty, value = next(encodedValue)
local typeImpl = types[ty]
if typeImpl == nil then
return false, "Couldn't decode value " .. tostring(ty)
end
return true, typeImpl.fromPod(value)
end
function EncodedValue.encode(rbxValue, propertyType)
assert(propertyType ~= nil, "Property type descriptor is required")
local typeImpl = types[propertyType]
if typeImpl == nil then
return false, ("Missing encoder for property type %q"):format(propertyType)
end
return true, {
[propertyType] = typeImpl.toPod(rbxValue),
}
end
return EncodedValue

View File

@@ -1,72 +0,0 @@
return function()
local HttpService = game:GetService("HttpService")
local EncodedValue = require(script.Parent.EncodedValue)
local allValues = require(script.Parent.allValues)
local function deepEq(a, b)
if typeof(a) ~= typeof(b) then
return false
end
local ty = typeof(a)
if ty == "table" then
local visited = {}
for key, valueA in pairs(a) do
visited[key] = true
if not deepEq(valueA, b[key]) then
return false
end
end
for key, valueB in pairs(b) do
if visited[key] then
continue
end
if not deepEq(valueB, a[key]) then
return false
end
end
return true
else
return a == b
end
end
local extraAssertions = {
CFrame = function(value)
expect(value).to.equal(CFrame.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12))
end,
}
for testName, testEntry in pairs(allValues) do
it("round trip " .. testName, function()
local ok, decoded = EncodedValue.decode(testEntry.value)
assert(ok, decoded)
if extraAssertions[testName] ~= nil then
extraAssertions[testName](decoded)
end
local ok, encoded = EncodedValue.encode(decoded, testEntry.ty)
assert(ok, encoded)
if not deepEq(encoded, testEntry.value) then
local expected = HttpService:JSONEncode(testEntry.value)
local actual = HttpService:JSONEncode(encoded)
local message = string.format(
"Round-trip results did not match.\nExpected:\n%s\nActual:\n%s",
expected, actual
)
error(message)
end
end)
end
end

View File

@@ -0,0 +1,2 @@
# rbx_dom_lua
Roblox Lua implementation of rbx-dom mechanisms, intended to work with rbx_dom_weak and friends.

View File

@@ -1,316 +0,0 @@
{
"Axes": {
"value": {
"Axes": [
"X",
"Y",
"Z"
]
},
"ty": "Axes"
},
"BinaryString": {
"value": {
"BinaryString": "SGVsbG8h"
},
"ty": "BinaryString"
},
"Bool": {
"value": {
"Bool": true
},
"ty": "Bool"
},
"BrickColor": {
"value": {
"BrickColor": 1004
},
"ty": "BrickColor"
},
"CFrame": {
"value": {
"CFrame": {
"position": [
1.0,
2.0,
3.0
],
"orientation": [
[
4.0,
5.0,
6.0
],
[
7.0,
8.0,
9.0
],
[
10.0,
11.0,
12.0
]
]
}
},
"ty": "CFrame"
},
"Color3": {
"value": {
"Color3": [
1.0,
2.0,
3.0
]
},
"ty": "Color3"
},
"Color3uint8": {
"value": {
"Color3uint8": [
0,
128,
255
]
},
"ty": "Color3uint8"
},
"ColorSequence": {
"value": {
"ColorSequence": {
"keypoints": [
{
"time": 0.0,
"color": [
1.0,
1.0,
0.5
]
},
{
"time": 1.0,
"color": [
0.0,
0.0,
0.0
]
}
]
}
},
"ty": "ColorSequence"
},
"Content": {
"value": {
"Content": "rbxassetid://12345"
},
"ty": "Content"
},
"Enum": {
"value": {
"Enum": 1234
},
"ty": "Enum"
},
"Faces": {
"value": {
"Faces": [
"Right",
"Top",
"Back",
"Left",
"Bottom",
"Front"
]
},
"ty": "Faces"
},
"Float32": {
"value": {
"Float32": 15.0
},
"ty": "Float32"
},
"Float64": {
"value": {
"Float64": 15123.0
},
"ty": "Float64"
},
"Int32": {
"value": {
"Int32": 6014
},
"ty": "Int32"
},
"Int64": {
"value": {
"Int64": 23491023
},
"ty": "Int64"
},
"NumberRange": {
"value": {
"NumberRange": [
-36.0,
94.0
]
},
"ty": "NumberRange"
},
"NumberSequence": {
"value": {
"NumberSequence": {
"keypoints": [
{
"time": 0.0,
"value": 5.0,
"envelope": 2.0
},
{
"time": 1.0,
"value": 22.0,
"envelope": 0.0
}
]
}
},
"ty": "NumberSequence"
},
"PhysicalProperties-Custom": {
"value": {
"PhysicalProperties": {
"density": 0.5,
"friction": 1.0,
"elasticity": 0.0,
"frictionWeight": 50.0,
"elasticityWeight": 25.0
}
},
"ty": "PhysicalProperties"
},
"PhysicalProperties-Default": {
"value": {
"PhysicalProperties": "Default"
},
"ty": "PhysicalProperties"
},
"Ray": {
"value": {
"Ray": {
"origin": [
1.0,
2.0,
3.0
],
"direction": [
4.0,
5.0,
6.0
]
}
},
"ty": "Ray"
},
"Rect": {
"value": {
"Rect": [
[
0.0,
5.0
],
[
10.0,
15.0
]
]
},
"ty": "Rect"
},
"Region3int16": {
"value": {
"Region3int16": [
[
-10,
-5,
0
],
[
5,
10,
15
]
]
},
"ty": "Region3int16"
},
"String": {
"value": {
"String": "Hello, world!"
},
"ty": "String"
},
"UDim": {
"value": {
"UDim": [
1.0,
32
]
},
"ty": "UDim"
},
"UDim2": {
"value": {
"UDim2": [
[
-1.0,
100
],
[
1.0,
-100
]
]
},
"ty": "UDim2"
},
"Vector2": {
"value": {
"Vector2": [
-50.0,
50.0
]
},
"ty": "Vector2"
},
"Vector2int16": {
"value": {
"Vector2int16": [
-300,
300
]
},
"ty": "Vector2int16"
},
"Vector3": {
"value": {
"Vector3": [
-300.0,
0.0,
1500.0
]
},
"ty": "Vector3"
},
"Vector3int16": {
"value": {
"Vector3int16": [
60,
37,
-450
]
},
"ty": "Vector3int16"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"name": "empty_folder",
"name": "rbx_dom_lua",
"tree": {
"$path": "src"
}

View File

@@ -0,0 +1,242 @@
local base64 = require(script.Parent.base64)
local function identity(...)
return ...
end
local function unpackDecoder(f)
return function(value)
return f(unpack(value))
end
end
local function serializeFloat(value)
-- TODO: Figure out a better way to serialize infinity and NaN, neither of
-- which fit into JSON.
if value == math.huge or value == -math.huge then
return 999999999 * math.sign(value)
end
return value
end
local encoders
encoders = {
Bool = identity,
Content = identity,
Float32 = serializeFloat,
Float64 = serializeFloat,
Int32 = identity,
Int64 = identity,
String = identity,
BinaryString = base64.encode,
SharedString = base64.encode,
BrickColor = function(value)
return value.Number
end,
CFrame = function(value)
return {value:GetComponents()}
end,
Color3 = function(value)
return {value.r, value.g, value.b}
end,
NumberRange = function(value)
return {value.Min, value.Max}
end,
NumberSequence = function(value)
local keypoints = {}
for index, keypoint in ipairs(value.Keypoints) do
keypoints[index] = {
Time = keypoint.Time,
Value = keypoint.Value,
Envelope = keypoint.Envelope,
}
end
return {
Keypoints = keypoints,
}
end,
ColorSequence = function(value)
local keypoints = {}
for index, keypoint in ipairs(value.Keypoints) do
keypoints[index] = {
Time = keypoint.Time,
Color = encoders.Color3(keypoint.Value),
}
end
return {
Keypoints = keypoints,
}
end,
Rect = function(value)
return {
Min = {value.Min.X, value.Min.Y},
Max = {value.Max.X, value.Max.Y},
}
end,
UDim = function(value)
return {value.Scale, value.Offset}
end,
UDim2 = function(value)
return {value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset}
end,
Vector2 = function(value)
return {
serializeFloat(value.X),
serializeFloat(value.Y),
}
end,
Vector2int16 = function(value)
return {value.X, value.Y}
end,
Vector3 = function(value)
return {
serializeFloat(value.X),
serializeFloat(value.Y),
serializeFloat(value.Z),
}
end,
Vector3int16 = function(value)
return {value.X, value.Y, value.Z}
end,
PhysicalProperties = function(value)
if value == nil then
return nil
else
return {
Density = value.Density,
Friction = value.Friction,
Elasticity = value.Elasticity,
FrictionWeight = value.FrictionWeight,
ElasticityWeight = value.ElasticityWeight,
}
end
end,
Ref = function(value)
return nil
end,
}
local decoders = {
Bool = identity,
Content = identity,
Enum = identity,
Float32 = identity,
Float64 = identity,
Int32 = identity,
Int64 = identity,
String = identity,
BinaryString = base64.decode,
SharedString = base64.decode,
BrickColor = BrickColor.new,
CFrame = unpackDecoder(CFrame.new),
Color3 = unpackDecoder(Color3.new),
Color3uint8 = unpackDecoder(Color3.fromRGB),
NumberRange = unpackDecoder(NumberRange.new),
UDim = unpackDecoder(UDim.new),
UDim2 = unpackDecoder(UDim2.new),
Vector2 = unpackDecoder(Vector2.new),
Vector2int16 = unpackDecoder(Vector2int16.new),
Vector3 = unpackDecoder(Vector3.new),
Vector3int16 = unpackDecoder(Vector3int16.new),
Rect = function(value)
return Rect.new(value.Min[1], value.Min[2], value.Max[1], value.Max[2])
end,
NumberSequence = function(value)
local keypoints = {}
for index, keypoint in ipairs(value.Keypoints) do
keypoints[index] = NumberSequenceKeypoint.new(
keypoint.Time,
keypoint.Value,
keypoint.Envelope
)
end
return NumberSequence.new(keypoints)
end,
ColorSequence = function(value)
local keypoints = {}
for index, keypoint in ipairs(value.Keypoints) do
keypoints[index] = ColorSequenceKeypoint.new(
keypoint.Time,
Color3.new(unpack(keypoint.Color))
)
end
return ColorSequence.new(keypoints)
end,
PhysicalProperties = function(properties)
if properties == nil then
return nil
else
return PhysicalProperties.new(
properties.Density,
properties.Friction,
properties.Elasticity,
properties.FrictionWeight,
properties.ElasticityWeight
)
end
end,
Ref = function()
return nil
end,
}
local EncodedValue = {}
function EncodedValue.decode(encodedValue)
local decoder = decoders[encodedValue.Type]
if decoder ~= nil then
return true, decoder(encodedValue.Value)
end
return false, "Couldn't decode value " .. tostring(encodedValue.Type)
end
function EncodedValue.encode(rbxValue, propertyType)
assert(propertyType ~= nil, "Property type descriptor is required")
if propertyType.type == "Data" then
local encoder = encoders[propertyType.name]
if encoder == nil then
return false, ("Missing encoder for property type %q"):format(propertyType.name)
end
if encoder ~= nil then
return true, {
Type = propertyType.name,
Value = encoder(rbxValue),
}
end
elseif propertyType.type == "Enum" then
return true, {
Type = "Enum",
Value = rbxValue.Value,
}
end
return false, ("Unknown property descriptor type %q"):format(tostring(propertyType.type))
end
return EncodedValue

View File

@@ -0,0 +1,127 @@
return function()
local RbxDom = require(script.Parent)
local EncodedValue = require(script.Parent.EncodedValue)
it("should decode Rect values", function()
local input = {
Type = "Rect",
Value = {
Min = {1, 2},
Max = {3, 4},
},
}
local output = Rect.new(1, 2, 3, 4)
local ok, decoded = EncodedValue.decode(input)
assert(ok, decoded)
expect(decoded).to.equal(output)
end)
it("should decode ColorSequence values", function()
local input = {
Type = "ColorSequence",
Value = {
Keypoints = {
{
Time = 0,
Color = { 0.12, 0.34, 0.56 },
},
{
Time = 1,
Color = { 0.13, 0.33, 0.37 },
},
}
},
}
local output = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.new(0.12, 0.34, 0.56)),
ColorSequenceKeypoint.new(1, Color3.new(0.13, 0.33, 0.37)),
})
local ok, decoded = EncodedValue.decode(input)
assert(ok, decoded)
expect(decoded).to.equal(output)
end)
it("should decode NumberSequence values", function()
local input = {
Type = "NumberSequence",
Value = {
Keypoints = {
{
Time = 0,
Value = 0.5,
Envelope = 0,
},
{
Time = 1,
Value = 0.5,
Envelope = 0,
},
}
},
}
local output = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.5, 0),
NumberSequenceKeypoint.new(1, 0.5, 0),
})
local ok, decoded = EncodedValue.decode(input)
assert(ok, decoded)
expect(decoded).to.equal(output)
end)
it("should decode PhysicalProperties values", function()
local input = {
Type = "PhysicalProperties",
Value = {
Density = 0.1,
Friction = 0.2,
Elasticity = 0.3,
FrictionWeight = 0.4,
ElasticityWeight = 0.5,
},
}
local output = PhysicalProperties.new(
0.1,
0.2,
0.3,
0.4,
0.5
)
local ok, decoded = EncodedValue.decode(input)
assert(ok, decoded)
expect(decoded).to.equal(output)
end)
-- This part of rbx_dom_lua needs some work still.
itSKIP("should encode Rect values", function()
local input = Rect.new(10, 20, 30, 40)
local output = {
Type = "Rect",
Value = {
Min = {10, 20},
Max = {30, 40},
},
}
local descriptor = RbxDom.findCanonicalPropertyDescriptor("ImageLabel", "SliceCenter")
local ok, encoded = EncodedValue.encode(input, descriptor)
assert(ok, encoded)
expect(encoded.Type).to.equal(output.Type)
expect(encoded.Value.Min[1]).to.equal(output.Value.Min[1])
expect(encoded.Value.Min[2]).to.equal(output.Value.Min[2])
expect(encoded.Value.Max[1]).to.equal(output.Value.Max[1])
expect(encoded.Value.Max[2]).to.equal(output.Value.Max[2])
end)
end

View File

@@ -21,7 +21,7 @@ end
function PropertyDescriptor.fromRaw(data, className, propertyName)
return setmetatable({
scriptability = data.Scriptability,
scriptability = data.scriptability,
className = className,
name = propertyName,
}, PropertyDescriptor)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
return {
classes = require(script.classes)
}

View File

@@ -1,4 +1,4 @@
local database = require(script.database)
local ReflectionDatabase = require(script.ReflectionDatabase)
local Error = require(script.Error)
local PropertyDescriptor = require(script.PropertyDescriptor)
@@ -6,31 +6,29 @@ local function findCanonicalPropertyDescriptor(className, propertyName)
local currentClassName = className
repeat
local currentClass = database.Classes[currentClassName]
local currentClass = ReflectionDatabase.classes[currentClassName]
if currentClass == nil then
return currentClass
end
local propertyData = currentClass.Properties[propertyName]
local propertyData = currentClass.properties[propertyName]
if propertyData ~= nil then
local canonicalData = propertyData.Kind.Canonical
if canonicalData ~= nil then
if propertyData.isCanonical then
return PropertyDescriptor.fromRaw(propertyData, currentClassName, propertyName)
end
local aliasData = propertyData.Kind.Alias
if aliasData ~= nil then
if propertyData.canonicalName ~= nil then
return PropertyDescriptor.fromRaw(
currentClass.Properties[aliasData.AliasFor],
currentClass.properties[propertyData.canonicalName],
currentClassName,
aliasData.AliasFor)
propertyData.canonicalName)
end
return nil
end
currentClassName = currentClass.Superclass
currentClassName = currentClass.superclass
until currentClassName == nil
return nil
@@ -66,4 +64,4 @@ return {
findCanonicalPropertyDescriptor = findCanonicalPropertyDescriptor,
Error = Error,
EncodedValue = require(script.EncodedValue),
}
}

View File

@@ -0,0 +1,35 @@
{
"name": "rbx_dom_lua test place",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"RbxDom": {
"$path": "src"
},
"TestEZ": {
"$path": "modules/testez/lib"
}
},
"ServerScriptService": {
"$className": "ServerScriptService",
"Run Tests": {
"$path": "test.server.lua"
}
},
"Players": {
"$className": "Players",
"$properties": {
"CharacterAutoLoads": false
}
},
"HttpService": {
"$className": "HttpService",
"$properties": {
"HttpEnabled": true
}
}
}
}

View File

@@ -0,0 +1,7 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local LIB_ROOT = ReplicatedStorage.RbxDom
local TestEZ = require(ReplicatedStorage.TestEZ)
TestEZ.TestBootstrap:run({LIB_ROOT})

View File

@@ -5,9 +5,9 @@ local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
return strict("Config", {
isDevBuild = isDevBuild,
codename = "Epiphany",
version = {7, 0, 0, "-rc.1"},
expectedServerVersionString = "7.0 or newer",
protocolVersion = 4,
version = {6, 2, 0},
expectedServerVersionString = "6.0 or newer",
protocolVersion = 3,
defaultHost = "localhost",
defaultPort = 34872,
})

View File

@@ -146,7 +146,8 @@ return function()
id = "VALUE",
changedProperties = {
Value = {
String = "WORLD",
Type = "String",
Value = "WORLD",
},
},
})
@@ -175,7 +176,8 @@ return function()
changedClassName = "StringValue",
changedProperties = {
Value = {
String = "I am Root",
Type = "String",
Value = "I am Root",
},
},
})

View File

@@ -6,31 +6,29 @@
local RbxDom = require(script.Parent.Parent.Parent.RbxDom)
local Error = require(script.Parent.Error)
local function decodeValue(encodedValue, instanceMap)
local ty, value = next(encodedValue)
local function decodeValue(virtualValue, instanceMap)
-- Refs are represented as IDs in the same space that Rojo's protocol uses.
if ty == "Ref" then
if value == "00000000000000000000000000000000" then
if virtualValue.Type == "Ref" then
if virtualValue.Value == nil then
return true, nil
end
local instance = instanceMap.fromIds[value]
local instance = instanceMap.fromIds[virtualValue.Value]
if instance ~= nil then
return true, instance
else
return false, Error.new(Error.RefDidNotExist, {
encodedValue = encodedValue,
virtualValue = virtualValue,
})
end
end
local ok, decodedValue = RbxDom.EncodedValue.decode(encodedValue)
local ok, decodedValue = RbxDom.EncodedValue.decode(virtualValue)
if not ok then
return false, Error.new(Error.CannotDecodeValue, {
encodedValue = encodedValue,
virtualValue = virtualValue,
innerError = decodedValue,
})
end

View File

@@ -75,9 +75,7 @@ local function diff(instanceMap, virtualInstances, rootId)
changedProperties[propertyName] = virtualValue
end
else
-- virtualValue can be empty in certain cases, and this may print out nil to the user.
local propertyType = next(virtualValue)
Log.warn("Failed to decode property of type {}", propertyType)
Log.warn("Failed to decode property of type {}", virtualValue.Type)
end
else
local err = existingValueOrErr

View File

@@ -80,7 +80,8 @@ return function()
Name = "Value",
Properties = {
Value = {
String = "Hello, world!",
Type = "String",
Value = "Hello, world!",
},
},
Children = {},
@@ -106,9 +107,8 @@ return function()
local patchProperty = update.changedProperties["Value"]
expect(patchProperty).to.be.a("table")
local ty, value = next(patchProperty)
expect(ty).to.equal("String")
expect(value).to.equal("Hello, world!")
expect(patchProperty.Type).to.equal("String")
expect(patchProperty.Value).to.equal("Hello, world!")
end)
it("should generate an empty patch if no properties changed", function()
@@ -119,7 +119,8 @@ return function()
Name = "Value",
Properties = {
Value = {
String = "Hello, world!",
Type = "String",
Value = "Hello, world!",
},
},
Children = {},
@@ -144,7 +145,8 @@ return function()
Name = "Folder",
Properties = {
FAKE_PROPERTY = {
String = "Hello, world!",
Type = "String",
Value = "Hello, world!",
},
},
Children = {},
@@ -181,7 +183,8 @@ return function()
-- heat_xml is a serialization-only property that is not
-- exposed to Lua.
heat_xml = {
Float32 = 5,
Type = "Float32",
Value = 5,
},
},
Children = {},

View File

@@ -70,7 +70,7 @@ function reifyInner(instanceMap, virtualInstances, id, parentInstance, unapplied
for propertyName, virtualValue in pairs(virtualInstance.Properties) do
-- Because refs may refer to instances that we haven't constructed yet,
-- we defer applying any ref properties until all instances are created.
if next(virtualValue) == "Ref" then
if virtualValue.Type == "Ref" then
table.insert(deferredRefs, {
id = id,
instance = instance,
@@ -136,23 +136,23 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
end
for _, entry in ipairs(deferredRefs) do
local _, refId = next(entry.virtualValue)
local virtualValue = entry.virtualValue
if refId == nil then
if virtualValue.Value == nil then
continue
end
local targetInstance = instanceMap.fromIds[refId]
local targetInstance = instanceMap.fromIds[virtualValue.Value]
if targetInstance == nil then
markFailed(entry.id, entry.propertyName, entry.virtualValue)
markFailed(entry.id, entry.propertyName, virtualValue)
continue
end
local ok = setProperty(entry.instance, entry.propertyName, targetInstance)
if not ok then
markFailed(entry.id, entry.propertyName, entry.virtualValue)
markFailed(entry.id, entry.propertyName, virtualValue)
end
end
end
return reify
return reify

View File

@@ -54,7 +54,8 @@ return function()
Name = "Spaghetti",
Properties = {
Value = {
String = "Hello, world!",
Type = "String",
Value = "Hello, world!",
},
},
Children = {},
@@ -190,7 +191,8 @@ return function()
Name = "Child",
Properties = {
Value = {
Ref = "ROOT",
Type = "Ref",
Value = "ROOT",
},
},
Children = {},
@@ -217,7 +219,8 @@ return function()
Name = "Root",
Properties = {
Value = {
Ref = "EXISTING",
Type = "Ref",
Value = "EXISTING",
},
},
Children = {},
@@ -255,7 +258,8 @@ return function()
Name = "Child A",
Properties = {
Value = {
Ref = "Child B",
Type = "Ref",
Value = "Child B",
},
},
Children = {},
@@ -294,7 +298,8 @@ return function()
Name = "Root",
Properties = {
Value = {
Ref = "CHILD",
Type = "Ref",
Value = "CHILD",
},
},
Children = {"CHILD"},

View File

@@ -112,7 +112,6 @@ function ServeSession:start()
self.__apiContext:connect()
:andThen(function(serverInfo)
self:__setStatus(Status.Connected, serverInfo.projectName)
self:__applyGameAndPlaceId(serverInfo)
local rootInstanceId = serverInfo.rootInstanceId
@@ -130,16 +129,6 @@ function ServeSession:stop()
self:__stopInternal()
end
function ServeSession:__applyGameAndPlaceId(serverInfo)
if serverInfo.gameId ~= nil then
game:SetUniverseId(serverInfo.gameId)
end
if serverInfo.placeId ~= nil then
game:SetPlaceId(serverInfo.placeId)
end
end
function ServeSession:__onActiveScriptChanged(activeScript)
if not self.__openScriptsExternally then
Log.trace("Not opening script {} because feature not enabled.", activeScript)

View File

@@ -5,7 +5,10 @@ local strict = require(script.Parent.strict)
local RbxId = t.string
local ApiValue = t.keys(t.string)
local ApiValue = t.interface({
Type = t.string,
Value = t.optional(t.any),
})
local ApiInstanceMetadata = t.interface({
ignoreUnknownInstances = t.optional(t.boolean),
@@ -93,4 +96,4 @@ return strict("Types", {
VirtualInstance = ApiInstance,
VirtualMetadata = ApiInstanceMetadata,
VirtualValue = ApiValue,
})
})

View File

@@ -1,6 +1,7 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">

View File

@@ -1,52 +0,0 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="DataModel" referent="0">
<Properties>
<string name="Name">unresolved-values</string>
</Properties>
<Item class="Lighting" referent="1">
<Properties>
<string name="Name">Lighting</string>
<Color3 name="Ambient">
<R>1</R>
<G>0</G>
<B>0</B>
</Color3>
<token name="Technology">1</token>
</Properties>
</Item>
<Item class="Workspace" referent="2">
<Properties>
<string name="Name">Workspace</string>
</Properties>
<Item class="BoolValue" referent="3">
<Properties>
<string name="Name">Bool</string>
<bool name="Value">true</bool>
</Properties>
</Item>
<Item class="Part" referent="4">
<Properties>
<string name="Name">Color</string>
<Color3uint8 name="Color3uint8">8404992</Color3uint8>
</Properties>
</Item>
<Item class="NumberValue" referent="5">
<Properties>
<string name="Name">Float</string>
<double name="Value">123.5</double>
</Properties>
</Item>
<Item class="IntValue" referent="6">
<Properties>
<string name="Name">Int</string>
<int64 name="Value">65</int64>
</Properties>
</Item>
</Item>
</Item>
</roblox>

View File

@@ -1,43 +0,0 @@
{
"name": "unresolved-values",
"tree": {
"$className": "DataModel",
"Lighting": {
"$properties": {
"Technology": "Voxel",
"Ambient": [1, 0, 0]
}
},
"Workspace": {
"Color": {
"$className": "Part",
"$properties": {
"Color": [0.5, 0.25, 0]
}
},
"Bool": {
"$className": "BoolValue",
"$properties": {
"Value": true
}
},
"Int": {
"$className": "IntValue",
"$properties": {
"Value": 65
}
},
"Float": {
"$className": "NumberValue",
"$properties": {
"Value": 123.5
}
}
}
}
}

View File

@@ -11,7 +11,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: add_folder
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
id-3:
Children: []

View File

@@ -10,7 +10,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: add_folder
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
messageCursor: 0
sessionId: id-1

View File

@@ -1,14 +1,10 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
placeId: ~
projectName: add_folder
protocolVersion: 4
protocolVersion: 3
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
@@ -11,10 +10,10 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: edit_init
Parent: "00000000000000000000000000000000"
Parent: ~
Properties:
Source:
String: "-- Edited contents"
Type: String
Value: "-- Edited contents"
messageCursor: 1
sessionId: id-1

View File

@@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
@@ -11,10 +10,10 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: edit_init
Parent: "00000000000000000000000000000000"
Parent: ~
Properties:
Source:
String: "-- Original contents"
Type: String
Value: "-- Original contents"
messageCursor: 0
sessionId: id-1

View File

@@ -1,14 +1,10 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
placeId: ~
projectName: edit_init
protocolVersion: 4
protocolVersion: 3
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
---
messageCursor: 1
messages:
@@ -13,7 +12,7 @@ messages:
changedName: ~
changedProperties:
Source:
String: "-- Edited contents"
Type: String
Value: "-- Edited contents"
id: id-2
sessionId: id-1

View File

@@ -10,7 +10,7 @@ instances:
Metadata:
ignoreUnknownInstances: true
Name: empty
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
messageCursor: 0
sessionId: id-1

View File

@@ -1,14 +1,10 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
placeId: ~
projectName: empty
protocolVersion: 4
protocolVersion: 3
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -1,28 +0,0 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
Children:
- id-3
ClassName: Folder
Id: id-2
Metadata:
ignoreUnknownInstances: false
Name: empty_folder
Parent: "00000000000000000000000000000000"
Properties: {}
id-3:
Children: []
ClassName: Model
Id: id-3
Metadata:
ignoreUnknownInstances: false
Name: test
Parent: id-2
Properties: {}
messageCursor: 1
sessionId: id-1

View File

@@ -1,18 +0,0 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
Children: []
ClassName: Folder
Id: id-2
Metadata:
ignoreUnknownInstances: false
Name: empty_folder
Parent: "00000000000000000000000000000000"
Properties: {}
messageCursor: 0
sessionId: id-1

View File

@@ -1,14 +0,0 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
placeId: ~
projectName: empty_folder
protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -1,21 +0,0 @@
---
source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
---
messageCursor: 1
messages:
- added:
id-3:
Children: []
ClassName: Model
Id: id-3
Metadata:
ignoreUnknownInstances: false
Name: test
Parent: id-2
Properties: {}
removed: []
updated: []
sessionId: id-1

View File

@@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-10:
@@ -14,7 +13,8 @@ instances:
Parent: id-3
Properties:
Value:
String: "File #6"
Type: String
Value: "File #6"
id-11:
Children: []
ClassName: StringValue
@@ -25,7 +25,8 @@ instances:
Parent: id-3
Properties:
Value:
String: "File #7"
Type: String
Value: "File #7"
id-12:
Children: []
ClassName: StringValue
@@ -36,7 +37,8 @@ instances:
Parent: id-3
Properties:
Value:
String: "File #8"
Type: String
Value: "File #8"
id-13:
Children: []
ClassName: StringValue
@@ -47,7 +49,8 @@ instances:
Parent: id-3
Properties:
Value:
String: "File #9"
Type: String
Value: "File #9"
id-2:
Children:
- id-3
@@ -56,7 +59,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: move_folder_of_stuff
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
id-3:
Children:
@@ -87,7 +90,8 @@ instances:
Parent: id-3
Properties:
Value:
String: "File #0"
Type: String
Value: "File #0"
id-5:
Children: []
ClassName: StringValue
@@ -98,7 +102,8 @@ instances:
Parent: id-3
Properties:
Value:
String: "File #1"
Type: String
Value: "File #1"
id-6:
Children: []
ClassName: StringValue
@@ -109,7 +114,8 @@ instances:
Parent: id-3
Properties:
Value:
String: "File #2"
Type: String
Value: "File #2"
id-7:
Children: []
ClassName: StringValue
@@ -120,7 +126,8 @@ instances:
Parent: id-3
Properties:
Value:
String: "File #3"
Type: String
Value: "File #3"
id-8:
Children: []
ClassName: StringValue
@@ -131,7 +138,8 @@ instances:
Parent: id-3
Properties:
Value:
String: "File #4"
Type: String
Value: "File #4"
id-9:
Children: []
ClassName: StringValue
@@ -142,7 +150,7 @@ instances:
Parent: id-3
Properties:
Value:
String: "File #5"
Type: String
Value: "File #5"
messageCursor: 1
sessionId: id-1

View File

@@ -10,7 +10,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: move_folder_of_stuff
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
messageCursor: 0
sessionId: id-1

View File

@@ -1,14 +1,10 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
placeId: ~
projectName: move_folder_of_stuff
protocolVersion: 4
protocolVersion: 3
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
---
messageCursor: 1
messages:
@@ -16,7 +15,8 @@ messages:
Parent: id-3
Properties:
Value:
String: "File #6"
Type: String
Value: "File #6"
id-11:
Children: []
ClassName: StringValue
@@ -27,7 +27,8 @@ messages:
Parent: id-3
Properties:
Value:
String: "File #7"
Type: String
Value: "File #7"
id-12:
Children: []
ClassName: StringValue
@@ -38,7 +39,8 @@ messages:
Parent: id-3
Properties:
Value:
String: "File #8"
Type: String
Value: "File #8"
id-13:
Children: []
ClassName: StringValue
@@ -49,7 +51,8 @@ messages:
Parent: id-3
Properties:
Value:
String: "File #9"
Type: String
Value: "File #9"
id-3:
Children:
- id-4
@@ -79,7 +82,8 @@ messages:
Parent: id-3
Properties:
Value:
String: "File #0"
Type: String
Value: "File #0"
id-5:
Children: []
ClassName: StringValue
@@ -90,7 +94,8 @@ messages:
Parent: id-3
Properties:
Value:
String: "File #1"
Type: String
Value: "File #1"
id-6:
Children: []
ClassName: StringValue
@@ -101,7 +106,8 @@ messages:
Parent: id-3
Properties:
Value:
String: "File #2"
Type: String
Value: "File #2"
id-7:
Children: []
ClassName: StringValue
@@ -112,7 +118,8 @@ messages:
Parent: id-3
Properties:
Value:
String: "File #3"
Type: String
Value: "File #3"
id-8:
Children: []
ClassName: StringValue
@@ -123,7 +130,8 @@ messages:
Parent: id-3
Properties:
Value:
String: "File #4"
Type: String
Value: "File #4"
id-9:
Children: []
ClassName: StringValue
@@ -134,8 +142,8 @@ messages:
Parent: id-3
Properties:
Value:
String: "File #5"
Type: String
Value: "File #5"
removed: []
updated: []
sessionId: id-1

View File

@@ -10,7 +10,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: remove_file
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
messageCursor: 1
sessionId: id-1

View File

@@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
@@ -12,7 +11,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: remove_file
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
id-3:
Children: []
@@ -24,7 +23,7 @@ instances:
Parent: id-2
Properties:
Value:
String: This file will be removed!
Type: String
Value: This file will be removed!
messageCursor: 0
sessionId: id-1

View File

@@ -1,14 +1,10 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
placeId: ~
projectName: remove_file
protocolVersion: 4
protocolVersion: 3
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
@@ -13,7 +12,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: scripts
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
id-3:
Children: []
@@ -25,7 +24,8 @@ instances:
Parent: id-2
Properties:
Source:
String: "-- Hello, from bar!"
Type: String
Value: "-- Hello, from bar!"
id-4:
Children: []
ClassName: ModuleScript
@@ -36,7 +36,7 @@ instances:
Parent: id-2
Properties:
Source:
String: Updated foo!
Type: String
Value: Updated foo!
messageCursor: 1
sessionId: id-1

View File

@@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
@@ -13,7 +12,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: scripts
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
id-3:
Children: []
@@ -25,7 +24,8 @@ instances:
Parent: id-2
Properties:
Source:
String: "-- Hello, from bar!"
Type: String
Value: "-- Hello, from bar!"
id-4:
Children: []
ClassName: ModuleScript
@@ -36,7 +36,7 @@ instances:
Parent: id-2
Properties:
Source:
String: "-- Hello, from foo!"
Type: String
Value: "-- Hello, from foo!"
messageCursor: 0
sessionId: id-1

View File

@@ -1,14 +1,10 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
placeId: ~
projectName: scripts
protocolVersion: 4
protocolVersion: 3
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
---
messageCursor: 1
messages:
@@ -13,7 +12,7 @@ messages:
changedName: ~
changedProperties:
Source:
String: Updated foo!
Type: String
Value: Updated foo!
id: id-4
sessionId: id-1

View File

@@ -3,7 +3,20 @@ use std::{env, panic, process};
use backtrace::Backtrace;
use structopt::StructOpt;
use librojo::cli::Options;
use librojo::cli::{self, GlobalOptions, Options, Subcommand};
fn run(global: GlobalOptions, subcommand: Subcommand) -> anyhow::Result<()> {
match subcommand {
Subcommand::Init(init_options) => cli::init(init_options)?,
Subcommand::Serve(serve_options) => cli::serve(global, serve_options)?,
Subcommand::Build(build_options) => cli::build(build_options)?,
Subcommand::Upload(upload_options) => cli::upload(upload_options)?,
Subcommand::Doc => cli::doc()?,
Subcommand::Plugin(plugin_options) => cli::plugin(plugin_options)?,
}
Ok(())
}
fn main() {
panic::set_hook(Box::new(|panic_info| {
@@ -68,7 +81,7 @@ fn main() {
.write_style(options.global.color.into())
.init();
if let Err(err) = options.run() {
if let Err(err) = run(options.global, options.subcommand) {
log::error!("{:?}", err);
process::exit(1);
}

View File

@@ -6,7 +6,7 @@ use std::{
use crossbeam_channel::{select, Receiver, RecvError, Sender};
use jod_thread::JoinHandle;
use memofs::{IoResultExt, Vfs, VfsEvent};
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{RbxId, RbxValue};
use crate::{
message_queue::MessageQueue,
@@ -16,9 +16,6 @@ use crate::{
snapshot_middleware::{snapshot_from_vfs, snapshot_project_node},
};
/// Processes file change events, updates the DOM, and sends those updates
/// through a channel for other stuff to consume.
///
/// Owns the connection between Rojo's VFS and its DOM by holding onto another
/// thread that processes messages.
///
@@ -182,7 +179,7 @@ impl JobThreadContext {
InstigatingSource::Path(path) => fs::remove_file(path).unwrap(),
InstigatingSource::ProjectNode(_, _, _, _) => {
log::warn!(
"Cannot remove instance {:?}, it's from a project file",
"Cannot remove instance {}, it's from a project file",
id
);
}
@@ -190,12 +187,12 @@ impl JobThreadContext {
} else {
// TODO
log::warn!(
"Cannot remove instance {:?}, it is not an instigating source.",
"Cannot remove instance {}, it is not an instigating source.",
id
);
}
} else {
log::warn!("Cannot remove instance {:?}, it does not exist.", id);
log::warn!("Cannot remove instance {}, it does not exist.", id);
}
}
@@ -222,7 +219,7 @@ impl JobThreadContext {
{
match instigating_source {
InstigatingSource::Path(path) => {
if let Some(Variant::String(value)) = changed_value {
if let Some(RbxValue::String { value }) = changed_value {
fs::write(path, value).unwrap();
} else {
log::warn!("Cannot change Source to non-string value.");
@@ -230,14 +227,14 @@ impl JobThreadContext {
}
InstigatingSource::ProjectNode(_, _, _, _) => {
log::warn!(
"Cannot remove instance {:?}, it's from a project file",
"Cannot remove instance {}, it's from a project file",
id
);
}
}
} else {
log::warn!(
"Cannot update instance {:?}, it is not an instigating source.",
"Cannot update instance {}, it is not an instigating source.",
id
);
}
@@ -246,7 +243,7 @@ impl JobThreadContext {
}
}
} else {
log::warn!("Cannot update instance {:?}, it does not exist.", id);
log::warn!("Cannot update instance {}, it does not exist.", id);
}
}
@@ -257,7 +254,7 @@ impl JobThreadContext {
}
}
fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<AppliedPatchSet> {
fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: RbxId) -> Option<AppliedPatchSet> {
let metadata = tree
.get_metadata(id)
.expect("metadata missing for instance present in tree");
@@ -266,7 +263,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
Some(path) => path,
None => {
log::error!(
"Instance {:?} did not have an instigating source, but was considered for an update.",
"Instance {} did not have an instigating source, but was considered for an update.",
id
);
log::error!("This is a bug. Please file an issue!");

View File

@@ -1,88 +1,24 @@
use std::{
fs::File,
io::{BufWriter, Write},
path::{Path, PathBuf},
};
use anyhow::Context;
use fs_err::File;
use memofs::Vfs;
use structopt::StructOpt;
use thiserror::Error;
use tokio::runtime::Runtime;
use crate::serve_session::ServeSession;
use crate::{cli::BuildCommand, serve_session::ServeSession, snapshot::RojoTree};
use super::resolve_path;
const UNKNOWN_OUTPUT_KIND_ERR: &str = "Could not detect what kind of file to build. \
Expected output file to end in .rbxl, .rbxlx, .rbxm, or .rbxmx.";
/// Generates a model or place file from the Rojo project.
#[derive(Debug, StructOpt)]
pub struct BuildCommand {
/// Path to the project to serve. Defaults to the current directory.
#[structopt(default_value = "")]
pub project: PathBuf,
/// Where to output the result.
///
/// Should end in .rbxm, .rbxl, .rbxmx, or .rbxlx.
#[structopt(long, short)]
pub output: PathBuf,
/// Whether to automatically rebuild when any input files change.
#[structopt(long)]
pub watch: bool,
}
impl BuildCommand {
pub fn run(self) -> anyhow::Result<()> {
let project_path = resolve_path(&self.project);
let output_kind = detect_output_kind(&self.output).context(UNKNOWN_OUTPUT_KIND_ERR)?;
log::trace!("Constructing in-memory filesystem");
let vfs = Vfs::new_default();
vfs.set_watch_enabled(self.watch);
let session = ServeSession::new(vfs, &project_path)?;
let mut cursor = session.message_queue().cursor();
write_model(&session, &self.output, output_kind)?;
if self.watch {
let rt = Runtime::new().unwrap();
loop {
let receiver = session.message_queue().subscribe(cursor);
let (new_cursor, _patch_set) = rt.block_on(receiver).unwrap();
cursor = new_cursor;
write_model(&session, &self.output, output_kind)?;
}
}
Ok(())
}
}
/// The different kinds of output that Rojo can build to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OutputKind {
/// An XML model file.
Rbxmx,
/// An XML place file.
Rbxlx,
/// A binary model file.
Rbxm,
/// A binary place file.
Rbxl,
}
fn detect_output_kind(output: &Path) -> Option<OutputKind> {
let extension = output.extension()?.to_str()?;
fn detect_output_kind(options: &BuildCommand) -> Option<OutputKind> {
let extension = options.output.extension()?.to_str()?;
match extension {
"rbxlx" => Some(OutputKind::Rbxlx),
@@ -93,33 +29,57 @@ fn detect_output_kind(output: &Path) -> Option<OutputKind> {
}
}
#[derive(Debug, Error)]
enum Error {
#[error("Could not detect what kind of file to build. Expected output file to end in .rbxl, .rbxlx, .rbxm, or .rbxmx.")]
UnknownOutputKind,
}
fn xml_encode_config() -> rbx_xml::EncodeOptions {
rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
}
fn write_model(
session: &ServeSession,
output: &Path,
output_kind: OutputKind,
) -> anyhow::Result<()> {
println!("Building project '{}'", session.project_name());
pub fn build(options: BuildCommand) -> Result<(), anyhow::Error> {
log::trace!("Constructing in-memory filesystem");
let vfs = Vfs::new_default();
vfs.set_watch_enabled(options.watch);
let session = ServeSession::new(vfs, &options.absolute_project())?;
let mut cursor = session.message_queue().cursor();
{
let tree = session.tree();
write_model(&tree, &options)?;
}
if options.watch {
let mut rt = Runtime::new().unwrap();
loop {
let receiver = session.message_queue().subscribe(cursor);
let (new_cursor, _patch_set) = rt.block_on(receiver).unwrap();
cursor = new_cursor;
let tree = session.tree();
write_model(&tree, &options)?;
}
}
Ok(())
}
fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), anyhow::Error> {
let output_kind = detect_output_kind(&options).ok_or(Error::UnknownOutputKind)?;
log::debug!("Hoping to generate file of type {:?}", output_kind);
let tree = session.tree();
let root_id = tree.get_root_id();
log::trace!("Opening output file for write");
let mut file = BufWriter::new(File::create(output)?);
let file = File::create(&options.output)?;
let mut file = BufWriter::new(file);
match output_kind {
OutputKind::Rbxm => {
rbx_binary::to_writer(&mut file, tree.inner(), &[root_id])?;
}
OutputKind::Rbxl => {
let root_instance = tree.get_instance(root_id).unwrap();
let top_level_ids = root_instance.children();
rbx_binary::to_writer(&mut file, tree.inner(), top_level_ids)?;
}
OutputKind::Rbxmx => {
// Model files include the root instance of the tree and all its
// descendants.
@@ -128,22 +88,36 @@ fn write_model(
}
OutputKind::Rbxlx => {
// Place files don't contain an entry for the DataModel, but our
// WeakDom representation does.
// RbxTree representation does.
let root_instance = tree.get_instance(root_id).unwrap();
let top_level_ids = root_instance.children();
rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())?;
}
OutputKind::Rbxm => {
rbx_binary::encode(tree.inner(), &[root_id], &mut file)?;
}
OutputKind::Rbxl => {
log::warn!("Support for building binary places (rbxl) is still experimental.");
log::warn!("Using the XML place format (rbxlx) is recommended instead.");
log::warn!("For more info, see https://github.com/rojo-rbx/rojo/issues/180");
let root_instance = tree.get_instance(root_id).unwrap();
let top_level_ids = root_instance.children();
rbx_binary::encode(tree.inner(), top_level_ids, &mut file)?;
}
}
file.flush()?;
let filename = output
let filename = options
.output
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("<invalid utf-8>");
println!("Built project to {}", filename);
log::info!("Built project to {}", filename);
Ok(())
}

View File

@@ -1,12 +1,4 @@
use structopt::StructOpt;
/// Open Rojo's documentation in your browser.
#[derive(Debug, StructOpt)]
pub struct DocCommand {}
impl DocCommand {
pub fn run(self) -> anyhow::Result<()> {
opener::open("https://rojo.space/docs")?;
Ok(())
}
pub fn doc() -> Result<(), anyhow::Error> {
opener::open("https://rojo.space/docs")?;
Ok(())
}

View File

@@ -1,29 +0,0 @@
use std::path::PathBuf;
use anyhow::Context;
use structopt::StructOpt;
use crate::project::Project;
/// Reformat a Rojo project using the standard JSON formatting rules.
#[derive(Debug, StructOpt)]
pub struct FmtProjectCommand {
/// Path to the project to format. Defaults to the current directory.
#[structopt(default_value = "")]
pub project: PathBuf,
}
impl FmtProjectCommand {
pub fn run(self) -> anyhow::Result<()> {
let project = Project::load_fuzzy(&self.project)?
.context("A project file is required to run 'rojo fmt-project'")?;
let serialized = serde_json::to_string_pretty(&project)
.context("could not re-encode project file as JSON")?;
fs_err::write(&project.file_location, &serialized)
.context("could not write back to project file")?;
Ok(())
}
}

View File

@@ -1,14 +1,13 @@
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str::FromStr;
use std::{
fs::{self, OpenOptions},
io::{self, Write},
path::Path,
process::{Command, Stdio},
};
use anyhow::{bail, format_err};
use fs_err as fs;
use fs_err::OpenOptions;
use structopt::StructOpt;
use thiserror::Error;
use super::resolve_path;
use crate::cli::{InitCommand, InitKind};
static MODEL_PROJECT: &str =
include_str!("../../assets/default-model-project/default.project.json");
@@ -21,71 +20,37 @@ static PLACE_PROJECT: &str =
static PLACE_README: &str = include_str!("../../assets/default-place-project/README.md");
static PLACE_GIT_IGNORE: &str = include_str!("../../assets/default-place-project/gitignore.txt");
/// Initializes a new Rojo project.
#[derive(Debug, StructOpt)]
pub struct InitCommand {
/// Path to the place to create the project. Defaults to the current directory.
#[structopt(default_value = "")]
pub path: PathBuf,
#[derive(Debug, Error)]
enum Error {
#[error("A project file named default.project.json already exists in this folder")]
AlreadyExists,
/// The kind of project to create, 'place' or 'model'. Defaults to place.
#[structopt(long, default_value = "place")]
pub kind: InitKind,
#[error("git init failed")]
GitInit,
}
impl InitCommand {
pub fn run(self) -> anyhow::Result<()> {
let base_path = resolve_path(&self.path);
fs::create_dir_all(&base_path)?;
pub fn init(options: InitCommand) -> Result<(), anyhow::Error> {
let base_path = options.absolute_path();
fs::create_dir_all(&base_path)?;
let canonical = fs::canonicalize(&base_path)?;
let project_name = canonical
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("new-project");
let canonical = fs::canonicalize(&base_path)?;
let project_name = canonical
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("new-project");
let project_params = ProjectParams {
name: project_name.to_owned(),
};
let project_params = ProjectParams {
name: project_name.to_owned(),
};
match self.kind {
InitKind::Place => init_place(&base_path, project_params)?,
InitKind::Model => init_model(&base_path, project_params)?,
}
println!("Created project successfully.");
Ok(())
match options.kind {
InitKind::Place => init_place(&base_path, project_params),
InitKind::Model => init_model(&base_path, project_params),
}
}
/// The templates we support for initializing a Rojo project.
#[derive(Debug, Clone, Copy)]
pub enum InitKind {
/// A place that contains a baseplate.
Place,
/// An empty model, suitable for a library or plugin.
Model,
}
impl FromStr for InitKind {
type Err = anyhow::Error;
fn from_str(source: &str) -> Result<Self, Self::Err> {
match source {
"place" => Ok(InitKind::Place),
"model" => Ok(InitKind::Model),
_ => Err(format_err!(
"Invalid init kind '{}'. Valid kinds are: place, model",
source
)),
}
}
}
fn init_place(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> {
println!("Creating new place project '{}'", project_params.name);
fn init_place(base_path: &Path, project_params: ProjectParams) -> Result<(), anyhow::Error> {
eprintln!("Creating new place project '{}'", project_params.name);
let project_file = project_params.render_template(PLACE_PROJECT);
try_create_project(base_path, &project_file)?;
@@ -123,11 +88,13 @@ fn init_place(base_path: &Path, project_params: ProjectParams) -> anyhow::Result
let git_ignore = project_params.render_template(PLACE_GIT_IGNORE);
try_git_init(base_path, &git_ignore)?;
eprintln!("Created project successfully.");
Ok(())
}
fn init_model(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> {
println!("Creating new model project '{}'", project_params.name);
fn init_model(base_path: &Path, project_params: ProjectParams) -> Result<(), anyhow::Error> {
eprintln!("Creating new model project '{}'", project_params.name);
let project_file = project_params.render_template(MODEL_PROJECT);
try_create_project(base_path, &project_file)?;
@@ -144,6 +111,8 @@ fn init_model(base_path: &Path, project_params: ProjectParams) -> anyhow::Result
let git_ignore = project_params.render_template(MODEL_GIT_IGNORE);
try_git_init(base_path, &git_ignore)?;
eprintln!("Created project successfully.");
Ok(())
}
@@ -169,7 +138,7 @@ fn try_git_init(path: &Path, git_ignore: &str) -> Result<(), anyhow::Error> {
let status = Command::new("git").arg("init").current_dir(path).status()?;
if !status.success() {
bail!("git init failed: status code {:?}", status.code());
return Err(Error::GitInit.into());
}
}
@@ -226,15 +195,13 @@ fn try_create_project(base_path: &Path, contents: &str) -> Result<(), anyhow::Er
let file_res = OpenOptions::new()
.write(true)
.create_new(true)
.open(&project_path);
.open(project_path);
let mut file = match file_res {
Ok(file) => file,
Err(err) => {
return match err.kind() {
io::ErrorKind::AlreadyExists => {
bail!("Project file already exists: {}", project_path.display())
}
io::ErrorKind::AlreadyExists => Err(Error::AlreadyExists.into()),
_ => Err(err.into()),
}
}

View File

@@ -2,24 +2,30 @@
mod build;
mod doc;
mod fmt_project;
mod init;
mod plugin;
mod serve;
mod upload;
use std::{borrow::Cow, env, path::Path, str::FromStr};
use std::{
borrow::Cow,
env,
error::Error,
fmt,
net::IpAddr,
path::{Path, PathBuf},
str::FromStr,
};
use structopt::StructOpt;
use thiserror::Error;
pub use self::build::BuildCommand;
pub use self::doc::DocCommand;
pub use self::fmt_project::FmtProjectCommand;
pub use self::init::{InitCommand, InitKind};
pub use self::plugin::{PluginCommand, PluginSubcommand};
pub use self::serve::ServeCommand;
pub use self::upload::UploadCommand;
pub use self::build::*;
pub use self::doc::*;
pub use self::init::*;
pub use self::plugin::*;
pub use self::serve::*;
pub use self::upload::*;
/// Command line options that Rojo accepts, defined using the structopt crate.
#[derive(Debug, StructOpt)]
@@ -33,20 +39,6 @@ pub struct Options {
pub subcommand: Subcommand,
}
impl Options {
pub fn run(self) -> anyhow::Result<()> {
match self.subcommand {
Subcommand::Init(subcommand) => subcommand.run(),
Subcommand::Serve(subcommand) => subcommand.run(self.global),
Subcommand::Build(subcommand) => subcommand.run(),
Subcommand::Upload(subcommand) => subcommand.run(),
Subcommand::FmtProject(subcommand) => subcommand.run(),
Subcommand::Doc(subcommand) => subcommand.run(),
Subcommand::Plugin(subcommand) => subcommand.run(),
}
}
}
#[derive(Debug, StructOpt)]
pub struct GlobalOptions {
/// Sets verbosity level. Can be specified multiple times.
@@ -108,19 +100,218 @@ pub struct ColorChoiceParseError {
#[derive(Debug, StructOpt)]
pub enum Subcommand {
/// Creates a new Rojo project.
Init(InitCommand),
/// Serves the project's files for use with the Rojo Studio plugin.
Serve(ServeCommand),
/// Generates a model or place file from the project.
Build(BuildCommand),
/// Generates a place or model file out of the project and uploads it to Roblox.
Upload(UploadCommand),
FmtProject(FmtProjectCommand),
Doc(DocCommand),
/// Open Rojo's documentation in your browser.
Doc,
/// Manages Rojo's Roblox Studio plugin.
Plugin(PluginCommand),
}
pub(super) fn resolve_path(path: &Path) -> Cow<'_, Path> {
/// Initializes a new Rojo project.
#[derive(Debug, StructOpt)]
pub struct InitCommand {
/// Path to the place to create the project. Defaults to the current directory.
#[structopt(default_value = "")]
pub path: PathBuf,
/// The kind of project to create, 'place' or 'model'. Defaults to place.
#[structopt(long, default_value = "place")]
pub kind: InitKind,
}
impl InitCommand {
pub fn absolute_path(&self) -> Cow<'_, Path> {
resolve_path(&self.path)
}
}
/// The templates we support for initializing a Rojo project.
#[derive(Debug, Clone, Copy)]
pub enum InitKind {
/// A place that matches what File -> New does in Roblox Studio.
Place,
/// An empty model, suitable for a library or plugin.
Model,
}
impl FromStr for InitKind {
type Err = InitKindParseError;
fn from_str(source: &str) -> Result<Self, Self::Err> {
match source {
"place" => Ok(InitKind::Place),
"model" => Ok(InitKind::Model),
_ => Err(InitKindParseError {
attempted: source.to_owned(),
}),
}
}
}
/// Error type for failing to parse an `InitKind`.
#[derive(Debug)]
pub struct InitKindParseError {
attempted: String,
}
impl Error for InitKindParseError {}
impl fmt::Display for InitKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(
formatter,
"Invalid init kind '{}'. Valid kinds are: place, model",
self.attempted
)
}
}
/// Expose a Rojo project through a web server that can communicate with the
/// Rojo Roblox Studio plugin, or be visited by the user in the browser.
#[derive(Debug, StructOpt)]
pub struct ServeCommand {
/// Path to the project to serve. Defaults to the current directory.
#[structopt(default_value = "")]
pub project: PathBuf,
/// The IP address to listen on. Defaults to `127.0.0.1`.
#[structopt(long)]
pub address: Option<IpAddr>,
/// The port to listen on. Defaults to the project's preference, or `34872` if
/// it has none.
#[structopt(long)]
pub port: Option<u16>,
}
impl ServeCommand {
pub fn absolute_project(&self) -> Cow<'_, Path> {
resolve_path(&self.project)
}
}
/// Build a Rojo project into a file.
#[derive(Debug, StructOpt)]
pub struct BuildCommand {
/// Path to the project to serve. Defaults to the current directory.
#[structopt(default_value = "")]
pub project: PathBuf,
/// Where to output the result.
#[structopt(long, short)]
pub output: PathBuf,
/// Whether to automatically rebuild when any input files change.
#[structopt(long)]
pub watch: bool,
}
impl BuildCommand {
pub fn absolute_project(&self) -> Cow<'_, Path> {
resolve_path(&self.project)
}
}
/// Build and upload a Rojo project to Roblox.com.
#[derive(Debug, StructOpt)]
pub struct UploadCommand {
/// Path to the project to upload. Defaults to the current directory.
#[structopt(default_value = "")]
pub project: PathBuf,
/// Authenication cookie to use. If not specified, Rojo will attempt to find one from the system automatically.
#[structopt(long)]
pub cookie: Option<String>,
/// Asset ID to upload to.
#[structopt(long = "asset_id")]
pub asset_id: u64,
}
impl UploadCommand {
pub fn absolute_project(&self) -> Cow<'_, Path> {
resolve_path(&self.project)
}
}
/// The kind of asset to upload to the website. Affects what endpoints Rojo uses
/// and changes how the asset is built.
#[derive(Debug, Clone, Copy)]
pub enum UploadKind {
/// Upload to a place.
Place,
/// Upload to a model-like asset, like a Model, Plugin, or Package.
Model,
}
impl FromStr for UploadKind {
type Err = UploadKindParseError;
fn from_str(source: &str) -> Result<Self, Self::Err> {
match source {
"place" => Ok(UploadKind::Place),
"model" => Ok(UploadKind::Model),
_ => Err(UploadKindParseError {
attempted: source.to_owned(),
}),
}
}
}
/// Error type for failing to parse an `UploadKind`.
#[derive(Debug)]
pub struct UploadKindParseError {
attempted: String,
}
impl Error for UploadKindParseError {}
impl fmt::Display for UploadKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(
formatter,
"Invalid upload kind '{}'. Valid kinds are: place, model",
self.attempted
)
}
}
fn resolve_path(path: &Path) -> Cow<'_, Path> {
if path.is_absolute() {
Cow::Borrowed(path)
} else {
Cow::Owned(env::current_dir().unwrap().join(path))
}
}
#[derive(Debug, StructOpt)]
pub enum PluginSubcommand {
/// Install the plugin in Roblox Studio's plugins folder. If the plugin is
/// already installed, installing it again will overwrite the current plugin
/// file.
Install,
/// Removes the plugin if it is installed.
Uninstall,
}
/// Install Rojo's plugin.
#[derive(Debug, StructOpt)]
pub struct PluginCommand {
#[structopt(subcommand)]
subcommand: PluginSubcommand,
}

View File

@@ -3,50 +3,26 @@ use std::{
io::BufWriter,
};
use anyhow::Result;
use memofs::{InMemoryFs, Vfs, VfsSnapshot};
use roblox_install::RobloxStudio;
use structopt::StructOpt;
use crate::serve_session::ServeSession;
use crate::{
cli::{PluginCommand, PluginSubcommand},
serve_session::ServeSession,
};
static PLUGIN_BINCODE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/plugin.bincode"));
static PLUGIN_FILE_NAME: &str = "RojoManagedPlugin.rbxm";
/// Install Rojo's plugin.
#[derive(Debug, StructOpt)]
pub struct PluginCommand {
#[structopt(subcommand)]
subcommand: PluginSubcommand,
}
/// Manages Rojo's Roblox Studio plugin.
#[derive(Debug, StructOpt)]
pub enum PluginSubcommand {
/// Install the plugin in Roblox Studio's plugins folder. If the plugin is
/// already installed, installing it again will overwrite the current plugin
/// file.
Install,
/// Removes the plugin if it is installed.
Uninstall,
}
impl PluginCommand {
pub fn run(self) -> anyhow::Result<()> {
self.subcommand.run()
pub fn plugin(options: PluginCommand) -> Result<()> {
match options.subcommand {
PluginSubcommand::Install => install_plugin(),
PluginSubcommand::Uninstall => uninstall_plugin(),
}
}
impl PluginSubcommand {
pub fn run(self) -> anyhow::Result<()> {
match self {
PluginSubcommand::Install => install_plugin(),
PluginSubcommand::Uninstall => uninstall_plugin(),
}
}
}
fn install_plugin() -> anyhow::Result<()> {
pub fn install_plugin() -> Result<()> {
let plugin_snapshot: VfsSnapshot = bincode::deserialize(PLUGIN_BINCODE)
.expect("Rojo's plugin was not properly packed into Rojo's binary");
@@ -73,12 +49,12 @@ fn install_plugin() -> anyhow::Result<()> {
let tree = session.tree();
let root_id = tree.get_root_id();
rbx_binary::to_writer(&mut file, tree.inner(), &[root_id])?;
rbx_binary::encode(tree.inner(), &[root_id], &mut file)?;
Ok(())
}
fn uninstall_plugin() -> anyhow::Result<()> {
fn uninstall_plugin() -> Result<()> {
let studio = RobloxStudio::locate()?;
let plugin_path = studio.plugins_path().join(PLUGIN_FILE_NAME);

View File

@@ -1,73 +1,52 @@
use std::{
io::{self, Write},
net::{IpAddr, Ipv4Addr},
path::PathBuf,
net::IpAddr,
net::Ipv4Addr,
sync::Arc,
};
use anyhow::Result;
use memofs::Vfs;
use structopt::StructOpt;
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
use crate::{serve_session::ServeSession, web::LiveServer};
use super::{resolve_path, GlobalOptions};
use crate::{
cli::{GlobalOptions, ServeCommand},
serve_session::ServeSession,
web::LiveServer,
};
const DEFAULT_BIND_ADDRESS: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
const DEFAULT_PORT: u16 = 34872;
/// Expose a Rojo project to the Rojo Studio plugin.
#[derive(Debug, StructOpt)]
pub struct ServeCommand {
/// Path to the project to serve. Defaults to the current directory.
#[structopt(default_value = "")]
pub project: PathBuf,
pub fn serve(global: GlobalOptions, options: ServeCommand) -> Result<()> {
let vfs = Vfs::new_default();
/// The IP address to listen on. Defaults to `127.0.0.1`.
#[structopt(long)]
pub address: Option<IpAddr>,
let session = Arc::new(ServeSession::new(vfs, &options.absolute_project())?);
/// The port to listen on. Defaults to the project's preference, or `34872` if
/// it has none.
#[structopt(long)]
pub port: Option<u16>,
}
let ip = options.address.unwrap_or(DEFAULT_BIND_ADDRESS.into());
impl ServeCommand {
pub fn run(self, global: GlobalOptions) -> anyhow::Result<()> {
let project_path = resolve_path(&self.project);
let port = options
.port
.or_else(|| session.project_port())
.unwrap_or(DEFAULT_PORT);
let vfs = Vfs::new_default();
let server = LiveServer::new(session);
let session = Arc::new(ServeSession::new(vfs, &project_path)?);
let _ = show_start_message(ip, port, global.color.into());
server.start((ip, port).into());
let ip = self.address.unwrap_or(DEFAULT_BIND_ADDRESS.into());
let port = self
.port
.or_else(|| session.project_port())
.unwrap_or(DEFAULT_PORT);
let server = LiveServer::new(session);
let _ = show_start_message(ip, port, global.color.into());
server.start((ip, port).into());
Ok(())
}
Ok(())
}
fn show_start_message(bind_address: IpAddr, port: u16, color: ColorChoice) -> io::Result<()> {
let mut green = ColorSpec::new();
green.set_fg(Some(Color::Green)).set_bold(true);
let writer = BufferWriter::stdout(color);
let mut buffer = writer.buffer();
writeln!(&mut buffer, "Rojo server listening:")?;
write!(&mut buffer, " Address: ")?;
buffer.set_color(&green)?;
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
if bind_address.is_loopback() {
writeln!(&mut buffer, "localhost")?;
} else {
@@ -76,7 +55,7 @@ fn show_start_message(bind_address: IpAddr, port: u16, color: ColorChoice) -> io
buffer.set_color(&ColorSpec::new())?;
write!(&mut buffer, " Port: ")?;
buffer.set_color(&green)?;
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
writeln!(&mut buffer, "{}", port)?;
writeln!(&mut buffer)?;
@@ -84,7 +63,7 @@ fn show_start_message(bind_address: IpAddr, port: u16, color: ColorChoice) -> io
buffer.set_color(&ColorSpec::new())?;
write!(&mut buffer, "Visit ")?;
buffer.set_color(&green)?;
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
write!(&mut buffer, "http://localhost:{}/", port)?;
buffer.set_color(&ColorSpec::new())?;

View File

@@ -1,87 +1,50 @@
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::{bail, format_err, Context};
use memofs::Vfs;
use reqwest::{
header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT},
StatusCode,
};
use structopt::StructOpt;
use thiserror::Error;
use crate::{auth_cookie::get_auth_cookie, serve_session::ServeSession};
use crate::{auth_cookie::get_auth_cookie, cli::UploadCommand, serve_session::ServeSession};
use super::resolve_path;
#[derive(Debug, Error)]
enum Error {
#[error("Rojo could not find your Roblox auth cookie. Please pass one via --cookie.")]
NeedAuthCookie,
/// Builds the project and uploads it to Roblox.
#[derive(Debug, StructOpt)]
pub struct UploadCommand {
/// Path to the project to upload. Defaults to the current directory.
#[structopt(default_value = "")]
pub project: PathBuf,
/// Authenication cookie to use. If not specified, Rojo will attempt to find one from the system automatically.
#[structopt(long)]
pub cookie: Option<String>,
/// Asset ID to upload to.
#[structopt(long = "asset_id")]
pub asset_id: u64,
#[error("The Roblox API returned an unexpected error: {body}")]
RobloxApi { body: String },
}
impl UploadCommand {
pub fn run(self) -> Result<(), anyhow::Error> {
let project_path = resolve_path(&self.project);
pub fn upload(options: UploadCommand) -> Result<(), anyhow::Error> {
let cookie = options
.cookie
.clone()
.or_else(get_auth_cookie)
.ok_or(Error::NeedAuthCookie)?;
let cookie = self.cookie.or_else(get_auth_cookie).context(
"Rojo could not find your Roblox auth cookie. Please pass one via --cookie.",
)?;
let vfs = Vfs::new_default();
let vfs = Vfs::new_default();
let session = ServeSession::new(vfs, &options.absolute_project())?;
let session = ServeSession::new(vfs, project_path)?;
let tree = session.tree();
let inner_tree = tree.inner();
let root_id = inner_tree.get_root_id();
let root_instance = inner_tree.get_instance(root_id).unwrap();
let tree = session.tree();
let inner_tree = tree.inner();
let root = inner_tree.root();
let encode_ids = match root_instance.class_name.as_str() {
"DataModel" => root_instance.get_children_ids().to_vec(),
_ => vec![root_id],
};
let encode_ids = match root.class.as_str() {
"DataModel" => root.children().to_vec(),
_ => vec![root.referent()],
};
let mut buffer = Vec::new();
let mut buffer = Vec::new();
log::trace!("Encoding XML model");
let config = rbx_xml::EncodeOptions::new()
.property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown);
log::trace!("Encoding binary model");
rbx_binary::to_writer(&mut buffer, tree.inner(), &encode_ids)?;
do_upload(buffer, self.asset_id, &cookie)
}
}
/// The kind of asset to upload to the website. Affects what endpoints Rojo uses
/// and changes how the asset is built.
#[derive(Debug, Clone, Copy)]
enum UploadKind {
/// Upload to a place.
Place,
/// Upload to a model-like asset, like a Model, Plugin, or Package.
Model,
}
impl FromStr for UploadKind {
type Err = anyhow::Error;
fn from_str(source: &str) -> Result<Self, Self::Err> {
match source {
"place" => Ok(UploadKind::Place),
"model" => Ok(UploadKind::Model),
attempted => Err(format_err!(
"Invalid upload kind '{}'. Valid kinds are: place, model",
attempted
)),
}
}
rbx_xml::to_writer(&mut buffer, tree.inner(), &encode_ids, config)?;
do_upload(buffer, options.asset_id, &cookie)
}
fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()> {
@@ -117,10 +80,10 @@ fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()>
let status = response.status();
if !status.is_success() {
bail!(
"The Roblox API returned an unexpected error: {}",
response.text()?
);
return Err(Error::RobloxApi {
body: response.text()?,
}
.into());
}
Ok(())

View File

@@ -15,7 +15,6 @@ mod message_queue;
mod multimap;
mod path_serializer;
mod project;
mod resolution;
mod serve_session;
mod session_id;
mod snapshot;

View File

@@ -1,5 +1,4 @@
//! Defines module for defining a small Lua AST for simple codegen. Rojo uses
//! this module to convert JSON into generated Lua code.
//! Defines module for defining a small Lua AST for simple codegen.
use std::{
fmt::{self, Write},

View File

@@ -1,6 +1,26 @@
use std::sync::{Mutex, RwLock};
use futures::channel::oneshot;
use futures::sync::oneshot;
struct Listener<T> {
sender: oneshot::Sender<(u32, Vec<T>)>,
cursor: u32,
}
fn fire_listener_if_ready<T: Clone>(
messages: &[T],
listener: Listener<T>,
) -> Result<(), Listener<T>> {
let current_cursor = messages.len() as u32;
if listener.cursor < current_cursor {
let new_messages = messages[(listener.cursor as usize)..].to_vec();
let _ = listener.sender.send((current_cursor, new_messages));
Ok(())
} else {
Err(listener)
}
}
/// A message queue with persistent history that can be subscribed to.
///
@@ -77,23 +97,3 @@ impl<T: Clone> MessageQueue<T> {
self.messages.read().unwrap().len() as u32
}
}
struct Listener<T> {
sender: oneshot::Sender<(u32, Vec<T>)>,
cursor: u32,
}
fn fire_listener_if_ready<T: Clone>(
messages: &[T],
listener: Listener<T>,
) -> Result<(), Listener<T>> {
let current_cursor = messages.len() as u32;
if listener.cursor < current_cursor {
let new_messages = messages[(listener.cursor as usize)..].to_vec();
let _ = listener.sender.send((current_cursor, new_messages));
Ok(())
} else {
Err(listener)
}
}

View File

@@ -4,10 +4,11 @@ use std::{
path::{Path, PathBuf},
};
use rbx_dom_weak::UnresolvedRbxValue;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{glob::Glob, resolution::UnresolvedValue};
use crate::glob::Glob;
static PROJECT_FILENAME: &str = "default.project.json";
@@ -57,16 +58,6 @@ pub struct Project {
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_place_ids: Option<HashSet<u64>>,
/// If specified, sets the current place's place ID when connecting to the
/// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
pub place_id: Option<u64>,
/// If specified, sets the current place's game ID when connecting to the
/// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
pub game_id: Option<u64>,
/// A list of globs, relative to the folder the project file is in, that
/// match files that should be excluded if Rojo encounters them.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -192,7 +183,7 @@ pub struct ProjectNode {
default,
skip_serializing_if = "HashMap::is_empty"
)]
pub properties: HashMap<String, UnresolvedValue>,
pub properties: HashMap<String, UnresolvedRbxValue>,
/// Defines the behavior when Rojo encounters unknown instances in Roblox
/// Studio during live sync. `$ignoreUnknownInstances` should be considered

View File

@@ -1,273 +0,0 @@
use std::borrow::Borrow;
use anyhow::format_err;
use rbx_dom_weak::types::{Color3, Content, Enum, Variant, VariantType, Vector2, Vector3};
use rbx_reflection::{DataType, PropertyDescriptor};
use serde::{Deserialize, Serialize};
/// A user-friendly version of `Variant` that supports specifying ambiguous
/// values. Ambiguous values need a reflection database to be resolved to a
/// usable value.
///
/// This type is used in Rojo projects and JSON models to make specifying the
/// most common types of properties, like strings or vectors, much easier.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UnresolvedValue {
FullyQualified(Variant),
Ambiguous(AmbiguousValue),
}
impl UnresolvedValue {
pub fn resolve(self, class_name: &str, prop_name: &str) -> anyhow::Result<Variant> {
match self {
UnresolvedValue::FullyQualified(full) => Ok(full),
UnresolvedValue::Ambiguous(partial) => partial.resolve(class_name, prop_name),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AmbiguousValue {
Bool(bool),
String(String),
Number(f64),
Array2([f64; 2]),
Array3([f64; 3]),
Array4([f64; 4]),
}
impl AmbiguousValue {
pub fn resolve(self, class_name: &str, prop_name: &str) -> anyhow::Result<Variant> {
let property = find_descriptor(class_name, prop_name)
.ok_or_else(|| format_err!("Unknown property {}.{}", class_name, prop_name))?;
match &property.data_type {
DataType::Enum(enum_name) => {
let database = rbx_reflection_database::get();
let enum_descriptor = database.enums.get(enum_name).ok_or_else(|| {
format_err!("Unknown enum {}. This is a Rojo bug!", enum_name)
})?;
let error = |what: &str| {
let mut all_values = enum_descriptor
.items
.keys()
.map(|value| value.borrow())
.collect::<Vec<_>>();
all_values.sort();
let examples = nonexhaustive_list(&all_values);
format_err!(
"Invalid value for property {}.{}. Got {} but \
expected a member of the {} enum such as {}",
class_name,
prop_name,
what,
enum_name,
examples,
)
};
let value = match self {
AmbiguousValue::String(value) => value,
unresolved => return Err(error(unresolved.describe())),
};
let resolved = enum_descriptor
.items
.get(value.as_str())
.ok_or_else(|| error(value.as_str()))?;
Ok(Enum::from_u32(*resolved).into())
}
DataType::Value(variant_ty) => match (variant_ty, self) {
(VariantType::Bool, AmbiguousValue::Bool(value)) => Ok(value.into()),
(VariantType::Float32, AmbiguousValue::Number(value)) => Ok((value as f32).into()),
(VariantType::Float64, AmbiguousValue::Number(value)) => Ok(value.into()),
(VariantType::Int32, AmbiguousValue::Number(value)) => Ok((value as i32).into()),
(VariantType::Int64, AmbiguousValue::Number(value)) => Ok((value as i64).into()),
(VariantType::String, AmbiguousValue::String(value)) => Ok(value.into()),
(VariantType::Content, AmbiguousValue::String(value)) => {
Ok(Content::from(value).into())
}
(VariantType::Vector2, AmbiguousValue::Array2(value)) => {
Ok(Vector2::new(value[0] as f32, value[1] as f32).into())
}
(VariantType::Vector3, AmbiguousValue::Array3(value)) => {
Ok(Vector3::new(value[0] as f32, value[1] as f32, value[2] as f32).into())
}
(VariantType::Color3, AmbiguousValue::Array3(value)) => {
Ok(Color3::new(value[0] as f32, value[1] as f32, value[2] as f32).into())
}
(_, unresolved) => Err(format_err!(
"Wrong type of value for property {}.{}. Expected {:?}, got {}",
class_name,
prop_name,
variant_ty,
unresolved.describe(),
)),
},
_ => Err(format_err!(
"Unknown data type for property {}.{}",
class_name,
prop_name
)),
}
}
fn describe(&self) -> &'static str {
match self {
AmbiguousValue::Bool(_) => "a bool",
AmbiguousValue::String(_) => "a string",
AmbiguousValue::Number(_) => "a number",
AmbiguousValue::Array2(_) => "an array of two numbers",
AmbiguousValue::Array3(_) => "an array of three numbers",
AmbiguousValue::Array4(_) => "an array of four numbers",
}
}
}
fn find_descriptor(
class_name: &str,
prop_name: &str,
) -> Option<&'static PropertyDescriptor<'static>> {
let database = rbx_reflection_database::get();
let mut current_class_name = class_name;
loop {
let class = database.classes.get(current_class_name)?;
if let Some(descriptor) = class.properties.get(prop_name) {
return Some(descriptor);
}
current_class_name = class.superclass.as_deref()?;
}
}
/// Outputs a string containing up to MAX_ITEMS entries from the given list. If
/// there are more than MAX_ITEMS items, the number of remaining items will be
/// listed.
fn nonexhaustive_list(values: &[&str]) -> String {
use std::fmt::Write;
const MAX_ITEMS: usize = 8;
let mut output = String::new();
let last_index = values.len() - 1;
let main_length = last_index.min(9);
let main_list = &values[..main_length];
for value in main_list {
output.push_str(value);
output.push_str(", ");
}
if values.len() > MAX_ITEMS {
write!(output, "or {} more", values.len() - main_length).unwrap();
} else {
output.push_str("or ");
output.push_str(values[values.len() - 1]);
}
output
}
#[cfg(test)]
mod test {
use super::*;
fn resolve(class: &str, prop: &str, json_value: &str) -> Variant {
let unresolved: UnresolvedValue = serde_json::from_str(json_value).unwrap();
unresolved.resolve(class, prop).unwrap()
}
#[test]
fn bools() {
assert_eq!(resolve("BoolValue", "Value", "false"), Variant::Bool(false));
// Script.Disabled is inherited from BaseScript
assert_eq!(resolve("Script", "Disabled", "true"), Variant::Bool(true));
}
#[test]
fn strings() {
// String literals can stay as strings
assert_eq!(
resolve("StringValue", "Value", "\"Hello!\""),
Variant::String("Hello!".into()),
);
// String literals can also turn into Content
assert_eq!(
resolve("Sky", "MoonTextureId", "\"rbxassetid://12345\""),
Variant::Content("rbxassetid://12345".into()),
);
// What about BinaryString values? For forward-compatibility reasons, we
// don't support any shorthands for BinaryString.
//
// assert_eq!(
// resolve("Folder", "Tags", "\"a\\u0000b\\u0000c\""),
// Variant::BinaryString(b"a\0b\0c".to_vec().into()),
// );
}
#[test]
fn numbers() {
assert_eq!(
resolve("Part", "CollisionGroupId", "123"),
Variant::Int32(123),
);
assert_eq!(
resolve("Folder", "SourceAssetId", "532413"),
Variant::Int64(532413),
);
assert_eq!(resolve("Part", "Transparency", "1"), Variant::Float32(1.0));
assert_eq!(resolve("NumberValue", "Value", "1"), Variant::Float64(1.0));
}
#[test]
fn vectors() {
assert_eq!(
resolve("ParticleEmitter", "SpreadAngle", "[1, 2]"),
Variant::Vector2(Vector2::new(1.0, 2.0)),
);
assert_eq!(
resolve("Part", "Position", "[4, 5, 6]"),
Variant::Vector3(Vector3::new(4.0, 5.0, 6.0)),
);
}
#[test]
fn colors() {
assert_eq!(
resolve("Part", "Color", "[1, 1, 1]"),
Variant::Color3(Color3::new(1.0, 1.0, 1.0)),
);
// There aren't any user-facing Color3uint8 properties. If there are
// some, we should treat them the same in the future.
}
#[test]
fn enums() {
assert_eq!(
resolve("Lighting", "Technology", "\"Voxel\""),
Variant::Enum(Enum::from_u32(1)),
);
}
}

View File

@@ -10,6 +10,7 @@ use std::{
use crossbeam_channel::Sender;
use memofs::IoResultExt;
use memofs::Vfs;
use rbx_dom_weak::RbxInstanceProperties;
use thiserror::Error;
use crate::{
@@ -18,15 +19,13 @@ use crate::{
project::{Project, ProjectError},
session_id::SessionId,
snapshot::{
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext, InstanceSnapshot,
PatchSet, RojoTree,
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext,
InstancePropertiesWithMeta, PatchSet, RojoTree,
},
snapshot_middleware::snapshot_from_vfs,
};
/// Contains all of the state for a Rojo serve session. A serve session is used
/// when we need to build a Rojo tree and possibly rebuild it when input files
/// change.
/// Contains all of the state for a Rojo serve session.
///
/// Nothing here is specific to any Rojo interface. Though the primary way to
/// interact with a serve session is Rojo's HTTP right now, there's no reason
@@ -119,7 +118,14 @@ impl ServeSession {
}
};
let mut tree = RojoTree::new(InstanceSnapshot::new());
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "ROOT".to_owned(),
class_name: "Folder".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
});
let root_id = tree.get_root_id();
@@ -197,14 +203,6 @@ impl ServeSession {
self.root_project.serve_port
}
pub fn place_id(&self) -> Option<u64> {
self.root_project.place_id
}
pub fn game_id(&self) -> Option<u64> {
self.root_project.game_id
}
pub fn start_time(&self) -> Instant {
self.start_time
}

View File

@@ -2,10 +2,7 @@
use std::{borrow::Cow, collections::HashMap};
use rbx_dom_weak::{
types::{Ref, Variant},
WeakDom,
};
use rbx_dom_weak::{RbxId, RbxTree, RbxValue};
use serde::{Deserialize, Serialize};
use super::InstanceMetadata;
@@ -14,12 +11,11 @@ use super::InstanceMetadata;
///
// Possible future improvements:
// - Use refcounted/interned strings
// - Replace use of Variant with a sum of Variant + borrowed value
// - Replace use of RbxValue with a sum of RbxValue + borrowed value
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InstanceSnapshot {
// FIXME: Don't use Option<Ref> anymore!
/// A temporary ID applied to the snapshot that's used for Ref properties.
pub snapshot_id: Option<Ref>,
pub snapshot_id: Option<RbxId>,
/// Rojo-specific metadata associated with the instance.
pub metadata: InstanceMetadata,
@@ -31,7 +27,7 @@ pub struct InstanceSnapshot {
pub class_name: Cow<'static, str>,
/// All other properties of the instance, weakly-typed.
pub properties: HashMap<String, Variant>,
pub properties: HashMap<String, RbxValue>,
/// The children of the instance represented as more snapshots.
///
@@ -65,16 +61,7 @@ impl InstanceSnapshot {
}
}
pub fn property<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: Into<Variant>,
{
self.properties.insert(key.into(), value.into());
self
}
pub fn properties(self, properties: impl Into<HashMap<String, Variant>>) -> Self {
pub fn properties(self, properties: impl Into<HashMap<String, RbxValue>>) -> Self {
Self {
properties: properties.into(),
..self
@@ -88,13 +75,6 @@ impl InstanceSnapshot {
}
}
pub fn snapshot_id(self, snapshot_id: Option<Ref>) -> Self {
Self {
snapshot_id,
..self
}
}
pub fn metadata(self, metadata: impl Into<InstanceMetadata>) -> Self {
Self {
metadata: metadata.into(),
@@ -102,13 +82,15 @@ impl InstanceSnapshot {
}
}
pub fn from_tree(tree: &WeakDom, id: Ref) -> Self {
let instance = tree.get_by_ref(id).expect("instance did not exist in tree");
pub fn from_tree(tree: &RbxTree, id: RbxId) -> Self {
let instance = tree
.get_instance(id)
.expect("instance did not exist in tree");
let children = instance
.children()
.get_children_ids()
.iter()
.copied()
.cloned()
.map(|id| Self::from_tree(tree, id))
.collect();
@@ -116,7 +98,7 @@ impl InstanceSnapshot {
snapshot_id: Some(id),
metadata: InstanceMetadata::default(),
name: Cow::Owned(instance.name.clone()),
class_name: Cow::Owned(instance.class.clone()),
class_name: Cow::Owned(instance.class_name.clone()),
properties: instance.properties.clone(),
children,
}

View File

@@ -2,19 +2,19 @@
use std::collections::HashMap;
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{RbxId, RbxValue};
use serde::{Deserialize, Serialize};
use super::{InstanceMetadata, InstanceSnapshot};
/// A set of different kinds of patches that can be applied to an WeakDom.
/// A set of different kinds of patches that can be applied to an RbxTree.
///
/// These patches shouldn't be persisted: there's no mechanism in place to make
/// sure that another patch wasn't applied before this one that could cause a
/// conflict!
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct PatchSet {
pub removed_instances: Vec<Ref>,
pub removed_instances: Vec<RbxId>,
pub added_instances: Vec<PatchAdd>,
pub updated_instances: Vec<PatchUpdate>,
}
@@ -32,20 +32,20 @@ impl<'a> PatchSet {
/// A patch containing an instance that was added to the tree.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PatchAdd {
pub parent_id: Ref,
pub parent_id: RbxId,
pub instance: InstanceSnapshot,
}
/// A patch indicating that properties of an instance changed.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PatchUpdate {
pub id: Ref,
pub id: RbxId,
pub changed_name: Option<String>,
pub changed_class_name: Option<String>,
/// Contains all changed properties. If a property is assigned to `None`,
/// then that property has been removed.
pub changed_properties: HashMap<String, Option<Variant>>,
pub changed_properties: HashMap<String, Option<RbxValue>>,
/// Changed Rojo-specific metadata, if any of it changed.
pub changed_metadata: Option<InstanceMetadata>,
@@ -63,8 +63,8 @@ pub struct PatchUpdate {
// current values in all fields.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AppliedPatchSet {
pub removed: Vec<Ref>,
pub added: Vec<Ref>,
pub removed: Vec<RbxId>,
pub added: Vec<RbxId>,
pub updated: Vec<AppliedPatchUpdate>,
}
@@ -80,17 +80,17 @@ impl AppliedPatchSet {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppliedPatchUpdate {
pub id: Ref,
pub id: RbxId,
// TODO: Store previous values in order to detect application conflicts
pub changed_name: Option<String>,
pub changed_class_name: Option<String>,
pub changed_properties: HashMap<String, Option<Variant>>,
pub changed_properties: HashMap<String, Option<RbxValue>>,
pub changed_metadata: Option<InstanceMetadata>,
}
impl AppliedPatchUpdate {
pub fn new(id: Ref) -> Self {
pub fn new(id: RbxId) -> Self {
Self {
id,
changed_name: None,

View File

@@ -2,11 +2,11 @@
use std::collections::HashMap;
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{RbxId, RbxInstanceProperties, RbxValue};
use super::{
patch::{AppliedPatchSet, AppliedPatchUpdate, PatchSet, PatchUpdate},
InstanceSnapshot, RojoTree,
InstancePropertiesWithMeta, InstanceSnapshot, RojoTree,
};
/// Consumes the input `PatchSet`, applying all of its prescribed changes to the
@@ -37,7 +37,7 @@ pub fn apply_patch_set(tree: &mut RojoTree, patch_set: PatchSet) -> AppliedPatch
struct PatchApplyContext {
/// A map from transient snapshot IDs (generated by snapshot middleware) to
/// instance IDs in the actual tree. These are both the same data type so
/// that they fit into the same `Variant::Ref` type.
/// that they fit into the same `RbxValue::Ref` type.
///
/// At this point in the patch process, IDs in instance properties have been
/// partially translated from 'snapshot space' into 'tree space' by the
@@ -53,7 +53,7 @@ struct PatchApplyContext {
/// #2 should not occur in well-formed projects, but is indistinguishable
/// from #1 right now. It could happen if two model files try to reference
/// eachother.
snapshot_id_to_instance_id: HashMap<Ref, Ref>,
snapshot_id_to_instance_id: HashMap<RbxId, RbxId>,
/// The properties of instances added by the current `PatchSet`.
///
@@ -68,7 +68,7 @@ struct PatchApplyContext {
///
/// This doesn't affect updated instances, since they're always applied
/// after we've added all the instances from the patch.
added_instance_properties: HashMap<Ref, HashMap<String, Variant>>,
added_instance_properties: HashMap<RbxId, HashMap<String, RbxValue>>,
/// The current applied patch result, describing changes made to the tree.
applied_patch_set: AppliedPatchSet,
@@ -93,10 +93,11 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -
.expect("Invalid instance ID in deferred property map");
for (key, mut property_value) in properties {
if let Variant::Ref(referent) = property_value {
if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(&referent)
{
property_value = Variant::Ref(instance_referent);
if let RbxValue::Ref { value: Some(id) } = property_value {
if let Some(&instance_id) = context.snapshot_id_to_instance_id.get(&id) {
property_value = RbxValue::Ref {
value: Some(instance_id),
};
}
}
@@ -107,40 +108,50 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -
context.applied_patch_set
}
fn apply_remove_instance(context: &mut PatchApplyContext, tree: &mut RojoTree, removed_id: Ref) {
tree.remove(removed_id);
context.applied_patch_set.removed.push(removed_id);
fn apply_remove_instance(context: &mut PatchApplyContext, tree: &mut RojoTree, removed_id: RbxId) {
match tree.remove_instance(removed_id) {
Some(_) => context.applied_patch_set.removed.push(removed_id),
None => {
log::warn!(
"Patch misapplication: Tried to remove instance {} but it did not exist.",
removed_id
);
}
}
}
fn apply_add_child(
context: &mut PatchApplyContext,
tree: &mut RojoTree,
parent_id: Ref,
parent_id: RbxId,
snapshot: InstanceSnapshot,
) {
let snapshot_id = snapshot.snapshot_id;
let properties = snapshot.properties;
let children = snapshot.children;
let properties = InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: snapshot.name.into_owned(),
class_name: snapshot.class_name.into_owned(),
// Property application is deferred until after all children
// are constructed. This helps apply referents correctly.
let remaining_snapshot = InstanceSnapshot::new()
.name(snapshot.name)
.class_name(snapshot.class_name)
.metadata(snapshot.metadata)
.snapshot_id(snapshot.snapshot_id);
// Property assignment is deferred until after we know about all
// instances in this patch. See `PatchApplyContext` for details.
properties: HashMap::new(),
},
metadata: snapshot.metadata,
};
let id = tree.insert_instance(properties, parent_id);
let id = tree.insert_instance(parent_id, remaining_snapshot);
context.applied_patch_set.added.push(id);
context.added_instance_properties.insert(id, properties);
context
.added_instance_properties
.insert(id, snapshot.properties);
if let Some(snapshot_id) = snapshot_id {
if let Some(snapshot_id) = snapshot.snapshot_id {
context.snapshot_id_to_instance_id.insert(snapshot_id, id);
}
for child in children {
apply_add_child(context, tree, id, child);
for child_snapshot in snapshot.children {
apply_add_child(context, tree, id, child_snapshot);
}
}
@@ -156,7 +167,7 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
Some(instance) => instance,
None => {
log::warn!(
"Patch misapplication: Instance {:?}, referred to by update patch, did not exist.",
"Patch misapplication: Instance {}, referred to by update patch, did not exist.",
patch.id
);
return;
@@ -178,24 +189,23 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
// Ref values need to be potentially rewritten from snapshot IDs to
// instance IDs if they referred to an instance that was created as
// part of this patch.
Some(Variant::Ref(referent)) => {
if referent.is_none() {
continue;
}
Some(RbxValue::Ref { value: Some(id) }) => {
// If our ID is not found in this map, then it either refers to
// an existing instance NOT added by this patch, or there was an
// error. See `PatchApplyContext::snapshot_id_to_instance_id`
// for more info.
let new_referent = context
let new_id = context
.snapshot_id_to_instance_id
.get(&referent)
.get(&id)
.copied()
.unwrap_or(referent);
.unwrap_or(id);
instance
.properties_mut()
.insert(key.clone(), Variant::Ref(new_referent));
instance.properties_mut().insert(
key.clone(),
RbxValue::Ref {
value: Some(new_id),
},
);
}
Some(ref value) => {
instance.properties_mut().insert(key.clone(), value.clone());
@@ -215,10 +225,10 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
mod test {
use super::*;
use std::borrow::Cow;
use std::{borrow::Cow, collections::HashMap};
use maplit::hashmap;
use rbx_dom_weak::types::Variant;
use rbx_dom_weak::RbxValue;
use super::super::PatchAdd;
@@ -226,7 +236,14 @@ mod test {
fn add_from_empty() {
let _ = env_logger::try_init();
let mut tree = RojoTree::new(InstanceSnapshot::new());
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "Folder".to_owned(),
class_name: "Folder".to_owned(),
properties: HashMap::new(),
},
metadata: Default::default(),
});
let root_id = tree.get_root_id();
@@ -236,7 +253,7 @@ mod test {
name: Cow::Borrowed("Foo"),
class_name: Cow::Borrowed("Bar"),
properties: hashmap! {
"Baz".to_owned() => Variant::Int32(5),
"Baz".to_owned() => RbxValue::Int32 { value: 5 },
},
children: Vec::new(),
};
@@ -265,14 +282,18 @@ mod test {
fn update_existing() {
let _ = env_logger::try_init();
let mut tree = RojoTree::new(
InstanceSnapshot::new()
.class_name("OldClassName")
.name("OldName")
.property("Foo", 7i32)
.property("Bar", 3i32)
.property("Unchanged", -5i32),
);
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "OldName".to_owned(),
class_name: "OldClassName".to_owned(),
properties: hashmap! {
"Foo".to_owned() => RbxValue::Int32 { value: 7 },
"Bar".to_owned() => RbxValue::Int32 { value: 3 },
"Unchanged".to_owned() => RbxValue::Int32 { value: -5 },
},
},
metadata: Default::default(),
});
let root_id = tree.get_root_id();
@@ -282,13 +303,13 @@ mod test {
changed_class_name: Some("NewClassName".to_owned()),
changed_properties: hashmap! {
// The value of Foo has changed
"Foo".to_owned() => Some(Variant::Int32(8)),
"Foo".to_owned() => Some(RbxValue::Int32 { value: 8 }),
// Bar has been deleted
"Bar".to_owned() => None,
// Baz has been added
"Baz".to_owned() => Some(Variant::Int32(10)),
"Baz".to_owned() => Some(RbxValue::Int32 { value: 10 }),
},
changed_metadata: None,
};
@@ -301,9 +322,9 @@ mod test {
apply_patch_set(&mut tree, patch_set);
let expected_properties = hashmap! {
"Foo".to_owned() => Variant::Int32(8),
"Baz".to_owned() => Variant::Int32(10),
"Unchanged".to_owned() => Variant::Int32(-5),
"Foo".to_owned() => RbxValue::Int32 { value: 8 },
"Baz".to_owned() => RbxValue::Int32 { value: 10 },
"Unchanged".to_owned() => RbxValue::Int32 { value: -5 },
};
let root_instance = tree.get_instance(root_id).unwrap();

View File

@@ -3,14 +3,14 @@
use std::collections::{HashMap, HashSet};
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{RbxId, RbxValue};
use super::{
patch::{PatchAdd, PatchSet, PatchUpdate},
InstanceSnapshot, InstanceWithMeta, RojoTree,
};
pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: Ref) -> PatchSet {
pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: RbxId) -> PatchSet {
let mut patch_set = PatchSet::new();
let mut context = ComputePatchContext::default();
@@ -26,15 +26,17 @@ pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: Ref)
#[derive(Default)]
struct ComputePatchContext {
snapshot_id_to_instance_id: HashMap<Ref, Ref>,
snapshot_id_to_instance_id: HashMap<RbxId, RbxId>,
}
fn rewrite_refs_in_updates(context: &ComputePatchContext, updates: &mut [PatchUpdate]) {
for update in updates {
for property_value in update.changed_properties.values_mut() {
if let Some(Variant::Ref(referent)) = property_value {
if let Some(&instance_ref) = context.snapshot_id_to_instance_id.get(referent) {
*property_value = Some(Variant::Ref(instance_ref));
if let Some(RbxValue::Ref { value: Some(id) }) = property_value {
if let Some(&instance_id) = context.snapshot_id_to_instance_id.get(id) {
*property_value = Some(RbxValue::Ref {
value: Some(instance_id),
});
}
}
}
@@ -49,9 +51,11 @@ fn rewrite_refs_in_additions(context: &ComputePatchContext, additions: &mut [Pat
fn rewrite_refs_in_snapshot(context: &ComputePatchContext, snapshot: &mut InstanceSnapshot) {
for property_value in snapshot.properties.values_mut() {
if let Variant::Ref(referent) = property_value {
if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(referent) {
*property_value = Variant::Ref(instance_referent);
if let RbxValue::Ref { value: Some(id) } = property_value {
if let Some(&instance_id) = context.snapshot_id_to_instance_id.get(id) {
*property_value = RbxValue::Ref {
value: Some(instance_id),
};
}
}
}
@@ -65,7 +69,7 @@ fn compute_patch_set_internal(
context: &mut ComputePatchContext,
snapshot: &InstanceSnapshot,
tree: &RojoTree,
id: Ref,
id: RbxId,
patch_set: &mut PatchSet,
) {
if let Some(snapshot_id) = snapshot.snapshot_id {
@@ -150,7 +154,7 @@ fn compute_children_patches(
context: &mut ComputePatchContext,
snapshot: &InstanceSnapshot,
tree: &RojoTree,
id: Ref,
id: RbxId,
patch_set: &mut PatchSet,
) {
let instance = tree
@@ -220,6 +224,9 @@ mod test {
use std::borrow::Cow;
use maplit::hashmap;
use rbx_dom_weak::RbxInstanceProperties;
use super::super::InstancePropertiesWithMeta;
/// This test makes sure that rewriting refs in instance update patches to
/// instances that already exists works. We should be able to correlate the
@@ -227,17 +234,26 @@ mod test {
/// value before returning from compute_patch_set.
#[test]
fn rewrite_ref_existing_instance_update() {
let tree = RojoTree::new(InstanceSnapshot::new().name("foo").class_name("foo"));
let tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "foo".to_owned(),
class_name: "foo".to_owned(),
properties: HashMap::new(),
},
metadata: Default::default(),
});
let root_id = tree.get_root_id();
// This snapshot should be identical to the existing tree except for the
// addition of a prop named Self, which is a self-referential Ref.
let snapshot_id = Ref::new();
let snapshot_id = RbxId::new();
let snapshot = InstanceSnapshot {
snapshot_id: Some(snapshot_id),
properties: hashmap! {
"Self".to_owned() => Variant::Ref(snapshot_id),
"Self".to_owned() => RbxValue::Ref {
value: Some(snapshot_id),
}
},
metadata: Default::default(),
@@ -254,7 +270,9 @@ mod test {
changed_name: None,
changed_class_name: None,
changed_properties: hashmap! {
"Self".to_owned() => Some(Variant::Ref(root_id)),
"Self".to_owned() => Some(RbxValue::Ref {
value: Some(root_id),
}),
},
changed_metadata: None,
}],
@@ -270,17 +288,26 @@ mod test {
/// one.
#[test]
fn rewrite_ref_existing_instance_addition() {
let tree = RojoTree::new(InstanceSnapshot::new().name("foo").class_name("foo"));
let tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "foo".to_owned(),
class_name: "foo".to_owned(),
properties: HashMap::new(),
},
metadata: Default::default(),
});
let root_id = tree.get_root_id();
// This patch describes the existing instance with a new child added.
let snapshot_id = Ref::new();
let snapshot_id = RbxId::new();
let snapshot = InstanceSnapshot {
snapshot_id: Some(snapshot_id),
children: vec![InstanceSnapshot {
properties: hashmap! {
"Self".to_owned() => Variant::Ref(snapshot_id),
"Self".to_owned() => RbxValue::Ref {
value: Some(snapshot_id),
},
},
snapshot_id: None,
@@ -305,7 +332,9 @@ mod test {
snapshot_id: None,
metadata: Default::default(),
properties: hashmap! {
"Self".to_owned() => Variant::Ref(root_id),
"Self".to_owned() => RbxValue::Ref {
value: Some(root_id),
},
},
name: Cow::Borrowed("child"),
class_name: Cow::Borrowed("child"),

View File

@@ -1,10 +1,11 @@
use insta::assert_yaml_snapshot;
use maplit::hashmap;
use rbx_dom_weak::{RbxInstanceProperties, RbxValue};
use rojo_insta_ext::RedactionMap;
use crate::{
snapshot::{apply_patch_set, InstanceSnapshot, PatchSet, PatchUpdate, RojoTree},
snapshot::{apply_patch_set, InstancePropertiesWithMeta, PatchSet, PatchUpdate, RojoTree},
tree_view::{intern_tree, view_tree},
};
@@ -48,7 +49,9 @@ fn add_property() {
changed_name: None,
changed_class_name: None,
changed_properties: hashmap! {
"Foo".to_owned() => Some("Value of Foo".into()),
"Foo".to_owned() => Some(RbxValue::String {
value: "Value of Foo".to_owned(),
}),
},
changed_metadata: None,
}],
@@ -75,9 +78,12 @@ fn remove_property() {
let root_id = tree.get_root_id();
let mut root_instance = tree.get_instance_mut(root_id).unwrap();
root_instance
.properties_mut()
.insert("Foo".to_owned(), "Should be removed".into());
root_instance.properties_mut().insert(
"Foo".to_owned(),
RbxValue::String {
value: "Should be removed".to_owned(),
},
);
}
let tree_view = view_tree(&tree, &mut redactions);
@@ -106,5 +112,12 @@ fn remove_property() {
}
fn empty_tree() -> RojoTree {
RojoTree::new(InstanceSnapshot::new().name("ROOT").class_name("ROOT"))
RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "ROOT".to_owned(),
class_name: "ROOT".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
})
}

View File

@@ -2,10 +2,11 @@ use std::borrow::Cow;
use insta::assert_yaml_snapshot;
use maplit::hashmap;
use rbx_dom_weak::{RbxInstanceProperties, RbxValue};
use rojo_insta_ext::RedactionMap;
use crate::snapshot::{compute_patch_set, InstanceSnapshot, RojoTree};
use crate::snapshot::{compute_patch_set, InstancePropertiesWithMeta, InstanceSnapshot, RojoTree};
#[test]
fn set_name_and_class_name() {
@@ -42,7 +43,9 @@ fn set_property() {
name: Cow::Borrowed("ROOT"),
class_name: Cow::Borrowed("ROOT"),
properties: hashmap! {
"PropertyName".to_owned() => "Hello, world!".into(),
"PropertyName".to_owned() => RbxValue::String {
value: "Hello, world!".to_owned(),
},
},
children: Vec::new(),
};
@@ -65,7 +68,9 @@ fn remove_property() {
let mut root_instance = tree.get_instance_mut(root_id).unwrap();
root_instance.properties_mut().insert(
"Foo".to_owned(),
"This should be removed by the patch.".into(),
RbxValue::String {
value: "This should be removed by the patch.".to_owned(),
},
);
}
@@ -123,8 +128,15 @@ fn remove_child() {
{
let root_id = tree.get_root_id();
let new_id = tree.insert_instance(
InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "Should not appear in snapshot".to_owned(),
class_name: "Folder".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
},
root_id,
InstanceSnapshot::new().name("Should not appear in snapshot"),
);
redactions.intern(new_id);
@@ -146,5 +158,12 @@ fn remove_child() {
}
fn empty_tree() -> RojoTree {
RojoTree::new(InstanceSnapshot::new().name("ROOT").class_name("ROOT"))
RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "ROOT".to_owned(),
class_name: "ROOT".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
})
}

View File

@@ -1,7 +1,6 @@
---
source: src/snapshot/tests/apply.rs
expression: applied_patch_value
---
removed: []
added: []
@@ -11,6 +10,6 @@ updated:
changed_class_name: ~
changed_properties:
Foo:
String: Value of Foo
Type: String
Value: Value of Foo
changed_metadata: ~

View File

@@ -1,17 +1,16 @@
---
source: src/snapshot/tests/apply.rs
expression: tree_view
---
id: id-1
name: ROOT
class_name: ROOT
properties:
Foo:
String: Value of Foo
Type: String
Value: Value of Foo
metadata:
ignore_unknown_instances: false
relevant_paths: []
context: {}
children: []

View File

@@ -1,17 +1,16 @@
---
source: src/snapshot/tests/apply.rs
expression: tree_view
---
id: id-1
name: ROOT
class_name: ROOT
properties:
Foo:
String: Should be removed
Type: String
Value: Should be removed
metadata:
ignore_unknown_instances: false
relevant_paths: []
context: {}
children: []

View File

@@ -1,7 +1,6 @@
---
source: src/snapshot/tests/compute.rs
expression: patch_value
---
removed_instances: []
added_instances: []
@@ -11,6 +10,6 @@ updated_instances:
changed_class_name: ~
changed_properties:
PropertyName:
String: "Hello, world!"
Type: String
Value: "Hello, world!"
changed_metadata: ~

View File

@@ -1,29 +1,26 @@
use std::{
collections::{HashMap, VecDeque},
collections::HashMap,
path::{Path, PathBuf},
};
use rbx_dom_weak::{
types::{Ref, Variant},
Instance, InstanceBuilder, WeakDom,
};
use rbx_dom_weak::{Descendants, RbxId, RbxInstance, RbxInstanceProperties, RbxTree, RbxValue};
use crate::multimap::MultiMap;
use super::{InstanceMetadata, InstanceSnapshot};
use super::InstanceMetadata;
/// An expanded variant of rbx_dom_weak's `WeakDom` that tracks additional
/// An expanded variant of rbx_dom_weak's `RbxTree` that tracks additional
/// metadata per instance that's Rojo-specific.
///
/// This tree is also optimized for doing fast incremental updates and patches.
#[derive(Debug)]
pub struct RojoTree {
/// Contains the instances without their Rojo-specific metadata.
inner: WeakDom,
inner: RbxTree,
/// Metadata associated with each instance that is kept up-to-date with the
/// set of actual instances.
metadata_map: HashMap<Ref, InstanceMetadata>,
metadata_map: HashMap<RbxId, InstanceMetadata>,
/// A multimap from source paths to all of the root instances that were
/// constructed from that path.
@@ -32,42 +29,31 @@ pub struct RojoTree {
/// value portion of the map is also a set in order to support the same path
/// appearing multiple times in the same Rojo project. This is sometimes
/// called "path aliasing" in various Rojo documentation.
path_to_ids: MultiMap<PathBuf, Ref>,
path_to_ids: MultiMap<PathBuf, RbxId>,
}
impl RojoTree {
pub fn new(snapshot: InstanceSnapshot) -> RojoTree {
let root_builder = InstanceBuilder::new(snapshot.class_name.to_owned())
.with_name(snapshot.name.to_owned())
.with_properties(snapshot.properties);
pub fn new(root: InstancePropertiesWithMeta) -> RojoTree {
let mut tree = RojoTree {
inner: WeakDom::new(root_builder),
inner: RbxTree::new(root.properties),
metadata_map: HashMap::new(),
path_to_ids: MultiMap::new(),
};
let root_ref = tree.inner.root_ref();
tree.insert_metadata(root_ref, snapshot.metadata);
for child in snapshot.children {
tree.insert_instance(root_ref, child);
}
tree.insert_metadata(tree.inner.get_root_id(), root.metadata);
tree
}
pub fn inner(&self) -> &WeakDom {
pub fn inner(&self) -> &RbxTree {
&self.inner
}
pub fn get_root_id(&self) -> Ref {
self.inner.root_ref()
pub fn get_root_id(&self) -> RbxId {
self.inner.get_root_id()
}
pub fn get_instance(&self, id: Ref) -> Option<InstanceWithMeta> {
if let Some(instance) = self.inner.get_by_ref(id) {
pub fn get_instance(&self, id: RbxId) -> Option<InstanceWithMeta> {
if let Some(instance) = self.inner.get_instance(id) {
let metadata = self.metadata_map.get(&id).unwrap();
Some(InstanceWithMeta { instance, metadata })
@@ -76,8 +62,8 @@ impl RojoTree {
}
}
pub fn get_instance_mut(&mut self, id: Ref) -> Option<InstanceWithMetaMut> {
if let Some(instance) = self.inner.get_by_ref_mut(id) {
pub fn get_instance_mut(&mut self, id: RbxId) -> Option<InstanceWithMetaMut> {
if let Some(instance) = self.inner.get_instance_mut(id) {
let metadata = self.metadata_map.get_mut(&id).unwrap();
Some(InstanceWithMetaMut { instance, metadata })
@@ -86,38 +72,38 @@ impl RojoTree {
}
}
pub fn insert_instance(&mut self, parent_ref: Ref, snapshot: InstanceSnapshot) -> Ref {
let builder = InstanceBuilder::new(snapshot.class_name.to_owned())
.with_name(snapshot.name.to_owned())
.with_properties(snapshot.properties);
let referent = self.inner.insert(parent_ref, builder);
self.insert_metadata(referent, snapshot.metadata);
for child in snapshot.children {
self.insert_instance(referent, child);
}
referent
pub fn insert_instance(
&mut self,
properties: InstancePropertiesWithMeta,
parent_id: RbxId,
) -> RbxId {
let id = self.inner.insert_instance(properties.properties, parent_id);
self.insert_metadata(id, properties.metadata);
id
}
pub fn remove(&mut self, id: Ref) {
let mut to_move = VecDeque::new();
to_move.push_back(id);
pub fn remove_instance(&mut self, id: RbxId) -> Option<RojoTree> {
if let Some(inner) = self.inner.remove_instance(id) {
let mut metadata_map = HashMap::new();
let mut path_to_ids = MultiMap::new();
while let Some(id) = to_move.pop_front() {
self.remove_metadata(id);
if let Some(instance) = self.inner.get_by_ref(id) {
to_move.extend(instance.children().iter().copied());
self.move_metadata(id, &mut metadata_map, &mut path_to_ids);
for instance in inner.descendants(id) {
self.move_metadata(instance.get_id(), &mut metadata_map, &mut path_to_ids);
}
}
self.inner.destroy(id);
Some(RojoTree {
inner,
metadata_map,
path_to_ids,
})
} else {
None
}
}
/// Replaces the metadata associated with the given instance ID.
pub fn update_metadata(&mut self, id: Ref, metadata: InstanceMetadata) {
pub fn update_metadata(&mut self, id: RbxId, metadata: InstanceMetadata) {
use std::collections::hash_map::Entry;
match self.metadata_map.entry(id) {
@@ -145,22 +131,22 @@ impl RojoTree {
}
}
pub fn descendants(&self, id: Ref) -> RojoDescendants<'_> {
let mut queue = VecDeque::new();
queue.push_back(id);
RojoDescendants { queue, tree: self }
pub fn descendants(&self, id: RbxId) -> RojoDescendants<'_> {
RojoDescendants {
inner: self.inner.descendants(id),
tree: self,
}
}
pub fn get_ids_at_path(&self, path: &Path) -> &[Ref] {
pub fn get_ids_at_path(&self, path: &Path) -> &[RbxId] {
self.path_to_ids.get(path)
}
pub fn get_metadata(&self, id: Ref) -> Option<&InstanceMetadata> {
pub fn get_metadata(&self, id: RbxId) -> Option<&InstanceMetadata> {
self.metadata_map.get(&id)
}
fn insert_metadata(&mut self, id: Ref, metadata: InstanceMetadata) {
fn insert_metadata(&mut self, id: RbxId, metadata: InstanceMetadata) {
for path in &metadata.relevant_paths {
self.path_to_ids.insert(path.clone(), id);
}
@@ -170,17 +156,25 @@ impl RojoTree {
/// Moves the Rojo metadata from the instance with the given ID from this
/// tree into some loose maps.
fn remove_metadata(&mut self, id: Ref) {
fn move_metadata(
&mut self,
id: RbxId,
metadata_map: &mut HashMap<RbxId, InstanceMetadata>,
path_to_ids: &mut MultiMap<PathBuf, RbxId>,
) {
let metadata = self.metadata_map.remove(&id).unwrap();
for path in &metadata.relevant_paths {
self.path_to_ids.remove(path, id);
path_to_ids.insert(path.clone(), id);
}
metadata_map.insert(id, metadata);
}
}
pub struct RojoDescendants<'a> {
queue: VecDeque<Ref>,
inner: Descendants<'a>,
tree: &'a RojoTree,
}
@@ -188,43 +182,50 @@ impl<'a> Iterator for RojoDescendants<'a> {
type Item = InstanceWithMeta<'a>;
fn next(&mut self) -> Option<Self::Item> {
let id = self.queue.pop_front()?;
let instance = self
.tree
.inner
.get_by_ref(id)
.expect("Instance did not exist");
let instance = self.inner.next()?;
let metadata = self
.tree
.get_metadata(instance.referent())
.get_metadata(instance.get_id())
.expect("Metadata did not exist for instance");
self.queue.extend(instance.children().iter().copied());
Some(InstanceWithMeta { instance, metadata })
}
}
/// RojoTree's equivalent of `&'a Instance`.
/// RojoTree's equivalent of `RbxInstanceProperties`.
#[derive(Debug, Clone)]
pub struct InstancePropertiesWithMeta {
pub properties: RbxInstanceProperties,
pub metadata: InstanceMetadata,
}
impl InstancePropertiesWithMeta {
pub fn new(properties: RbxInstanceProperties, metadata: InstanceMetadata) -> Self {
InstancePropertiesWithMeta {
properties,
metadata,
}
}
}
/// RojoTree's equivalent of `&'a RbxInstance`.
///
/// This has to be a value type for RojoTree because the instance and metadata
/// are stored in different places. The mutable equivalent is
/// `InstanceWithMetaMut`.
#[derive(Debug, Clone, Copy)]
pub struct InstanceWithMeta<'a> {
instance: &'a Instance,
instance: &'a RbxInstance,
metadata: &'a InstanceMetadata,
}
impl<'a> InstanceWithMeta<'a> {
pub fn id(&self) -> Ref {
self.instance.referent()
pub fn id(&self) -> RbxId {
self.instance.get_id()
}
pub fn parent(&self) -> Ref {
self.instance.parent()
pub fn parent(&self) -> Option<RbxId> {
self.instance.get_parent_id()
}
pub fn name(&self) -> &'a str {
@@ -232,15 +233,15 @@ impl<'a> InstanceWithMeta<'a> {
}
pub fn class_name(&self) -> &'a str {
&self.instance.class
&self.instance.class_name
}
pub fn properties(&self) -> &'a HashMap<String, Variant> {
pub fn properties(&self) -> &'a HashMap<String, RbxValue> {
&self.instance.properties
}
pub fn children(&self) -> &'a [Ref] {
self.instance.children()
pub fn children(&self) -> &'a [RbxId] {
self.instance.get_children_ids()
}
pub fn metadata(&self) -> &'a InstanceMetadata {
@@ -248,20 +249,20 @@ impl<'a> InstanceWithMeta<'a> {
}
}
/// RojoTree's equivalent of `&'a mut Instance`.
/// RojoTree's equivalent of `&'a mut RbxInstance`.
///
/// This has to be a value type for RojoTree because the instance and metadata
/// are stored in different places. The immutable equivalent is
/// `InstanceWithMeta`.
#[derive(Debug)]
pub struct InstanceWithMetaMut<'a> {
instance: &'a mut Instance,
instance: &'a mut RbxInstance,
metadata: &'a mut InstanceMetadata,
}
impl InstanceWithMetaMut<'_> {
pub fn id(&self) -> Ref {
self.instance.referent()
pub fn id(&self) -> RbxId {
self.instance.get_id()
}
pub fn name(&self) -> &str {
@@ -273,23 +274,23 @@ impl InstanceWithMetaMut<'_> {
}
pub fn class_name(&self) -> &str {
&self.instance.class
&self.instance.class_name
}
pub fn class_name_mut(&mut self) -> &mut String {
&mut self.instance.class
&mut self.instance.class_name
}
pub fn properties(&self) -> &HashMap<String, Variant> {
pub fn properties(&self) -> &HashMap<String, RbxValue> {
&self.instance.properties
}
pub fn properties_mut(&mut self) -> &mut HashMap<String, Variant> {
pub fn properties_mut(&mut self) -> &mut HashMap<String, RbxValue> {
&mut self.instance.properties
}
pub fn children(&self) -> &[Ref] {
self.instance.children()
pub fn children(&self) -> &[RbxId] {
self.instance.get_children_ids()
}
pub fn metadata(&self) -> &InstanceMetadata {

View File

@@ -3,6 +3,7 @@ use std::{collections::BTreeMap, path::Path};
use anyhow::Context;
use maplit::hashmap;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::RbxValue;
use serde::Serialize;
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
@@ -29,7 +30,9 @@ pub fn snapshot_csv(
.name(instance_name)
.class_name("LocalizationTable")
.properties(hashmap! {
"Contents".to_owned() => table_contents.into(),
"Contents".to_owned() => RbxValue::String {
value: table_contents,
},
})
.metadata(
InstanceMetadata::new()
@@ -38,8 +41,8 @@ pub fn snapshot_csv(
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
}
Ok(Some(snapshot))

View File

@@ -60,8 +60,8 @@ pub fn snapshot_dir(context: &InstanceContext, vfs: &Vfs, path: &Path) -> Snapsh
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = DirectoryMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
let mut metadata = DirectoryMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
}
Ok(Some(snapshot))

View File

@@ -3,6 +3,7 @@ use std::path::Path;
use anyhow::Context;
use maplit::hashmap;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::RbxValue;
use crate::{
lua_ast::{Expression, Statement},
@@ -25,7 +26,9 @@ pub fn snapshot_json(
let as_lua = json_to_lua(value).to_string();
let properties = hashmap! {
"Source".to_owned() => as_lua.into(),
"Source".to_owned() => RbxValue::String {
value: as_lua,
},
};
let meta_path = path.with_file_name(format!("{}.meta.json", instance_name));
@@ -42,8 +45,8 @@ pub fn snapshot_json(
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
}
Ok(Some(snapshot))

Some files were not shown because too many files have changed in this diff Show More