mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-20 12:45:05 +00:00
Compare commits
61 Commits
v6.0.0
...
v7.0.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af4a3ca0af | ||
|
|
43715143e4 | ||
|
|
16aa354d36 | ||
|
|
c739025453 | ||
|
|
f0526d17de | ||
|
|
6cc2e919c0 | ||
|
|
e1f9eaefa9 | ||
|
|
5d62bf9b60 | ||
|
|
4aa5814a0a | ||
|
|
5bca244062 | ||
|
|
7cf57714a4 | ||
|
|
92e6f862ad | ||
|
|
2377f41036 | ||
|
|
26a08f4d9f | ||
|
|
672d207961 | ||
|
|
a3d8e50f26 | ||
|
|
d3abca46a8 | ||
|
|
17fdd18c55 | ||
|
|
e482d038c6 | ||
|
|
d0482a004e | ||
|
|
561a3e3256 | ||
|
|
158dac5e1c | ||
|
|
1413f8c0b6 | ||
|
|
ffb2aa332a | ||
|
|
45e8208e9c | ||
|
|
7f230a8bf4 | ||
|
|
afe26b8c16 | ||
|
|
d153f62b8a | ||
|
|
5c80cd6e50 | ||
|
|
df1aced95d | ||
|
|
5a0a8f5077 | ||
|
|
1c281539e0 | ||
|
|
ef41d14f50 | ||
|
|
aa29397732 | ||
|
|
ca8865a3ce | ||
|
|
7599ef6626 | ||
|
|
43e71f9242 | ||
|
|
aac9b25efe | ||
|
|
532d170585 | ||
|
|
3dcb14013b | ||
|
|
a4c782cd35 | ||
|
|
0779baa0ac | ||
|
|
0849fab644 | ||
|
|
d85bca2e7e | ||
|
|
c7ab6c435c | ||
|
|
98db3b4f08 | ||
|
|
0e7ba839ed | ||
|
|
2c27691e57 | ||
|
|
934506bdfd | ||
|
|
f313fa4ae1 | ||
|
|
78755b6130 | ||
|
|
8bf59e5cbb | ||
|
|
8f71d13714 | ||
|
|
f4a790eb50 | ||
|
|
0d951c8ad1 | ||
|
|
59ef5f05ea | ||
|
|
b84aab0960 | ||
|
|
0e89b91c38 | ||
|
|
7888a704e1 | ||
|
|
804fd3de8e | ||
|
|
4992c36f08 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
rust_version: [stable, "1.43.1"]
|
rust_version: [stable, "1.46.0"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
|||||||
103
CHANGELOG.md
103
CHANGELOG.md
@@ -2,6 +2,109 @@
|
|||||||
|
|
||||||
## Unreleased Changes
|
## 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.
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
|
## [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.0.2](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.2) (February 9, 2021)
|
||||||
|
* Fixed `rojo upload` to handle CSRF challenges.
|
||||||
|
|
||||||
|
## [6.0.1](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.1) (January 22, 2021)
|
||||||
|
* Fixed `rojo upload` requests being rejected by Roblox
|
||||||
|
|
||||||
## [6.0.0](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.0) (January 16, 2021)
|
## [6.0.0](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.0) (January 16, 2021)
|
||||||
* Improved server error messages
|
* Improved server error messages
|
||||||
* The server will now keep running in more error cases
|
* The server will now keep running in more error cases
|
||||||
|
|||||||
@@ -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`
|
6. Tag the commit with the version from `Cargo.toml` prepended with a v, like `v0.4.13`
|
||||||
7. Publish the CLI
|
7. Publish the CLI
|
||||||
* `cargo publish`
|
* `cargo publish`
|
||||||
8. Build and upload the plugin
|
8. Publish the Plugin
|
||||||
|
* `rojo publish plugin --asset_id 6415005344`
|
||||||
* `rojo build plugin -o Rojo.rbxm`
|
* `rojo build plugin -o Rojo.rbxm`
|
||||||
* Upload `Rojo.rbxm` to Roblox.com, keep it for later
|
|
||||||
9. Push commits and tags
|
9. Push commits and tags
|
||||||
* `git push && git push --tags`
|
* `git push && git push --tags`
|
||||||
10. Copy GitHub release content from previous release
|
10. Copy GitHub release content from previous release
|
||||||
|
|||||||
1456
Cargo.lock
generated
1456
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
47
Cargo.toml
47
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "6.0.0"
|
version = "7.0.0-rc.1"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
description = "Enables professional-grade development tools for Roblox developers"
|
description = "Enables professional-grade development tools for Roblox developers"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
@@ -9,6 +9,7 @@ documentation = "https://rojo.space/docs"
|
|||||||
repository = "https://github.com/rojo-rbx/rojo"
|
repository = "https://github.com/rojo-rbx/rojo"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
exclude = [
|
exclude = [
|
||||||
"/test-projects/**",
|
"/test-projects/**",
|
||||||
@@ -45,48 +46,58 @@ name = "build"
|
|||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
memofs = { version = "0.1.2", path = "memofs" }
|
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"
|
||||||
|
|
||||||
anyhow = "1.0.27"
|
anyhow = "1.0.27"
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
bincode = "1.2.1"
|
bincode = "1.2.1"
|
||||||
crossbeam-channel = "0.4.0"
|
crossbeam-channel = "0.5.1"
|
||||||
csv = "1.1.1"
|
csv = "1.1.1"
|
||||||
env_logger = "0.7.1"
|
env_logger = "0.9.0"
|
||||||
fs-err = "2.2.0"
|
fs-err = "2.2.0"
|
||||||
futures = "0.1.29"
|
futures = "0.3.16"
|
||||||
globset = "0.4.4"
|
globset = "0.4.4"
|
||||||
humantime = "1.3.0"
|
humantime = "2.1.0"
|
||||||
hyper = "0.12.35"
|
hyper = { version = "0.14.11", features = ["server", "tcp", "http1"] }
|
||||||
jod-thread = "0.1.0"
|
jod-thread = "0.1.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
maplit = "1.0.1"
|
maplit = "1.0.1"
|
||||||
notify = "4.0.14"
|
notify = "4.0.14"
|
||||||
opener = "0.4.1"
|
opener = "0.5.0"
|
||||||
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"
|
regex = "1.3.1"
|
||||||
reqwest = "0.9.20"
|
reqwest = "0.9.20"
|
||||||
ritz = "0.1.0"
|
ritz = "0.1.0"
|
||||||
rlua = "0.17.0"
|
rlua = "0.17.0"
|
||||||
roblox_install = "0.2.2"
|
roblox_install = "1.0.0"
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
structopt = "0.3.5"
|
structopt = "0.3.5"
|
||||||
termcolor = "1.0.5"
|
termcolor = "1.0.5"
|
||||||
thiserror = "1.0.11"
|
thiserror = "1.0.11"
|
||||||
tokio = "0.1.22"
|
tokio = { version = "1.9.0", features = ["rt", "rt-multi-thread"] }
|
||||||
uuid = { version = "0.8.1", features = ["v4", "serde"] }
|
uuid = { version = "0.8.1", features = ["v4", "serde"] }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.6.2"
|
winreg = "0.9.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
memofs = { version = "0.1.3", path = "memofs" }
|
memofs = { version = "0.2.0", path = "memofs" }
|
||||||
|
|
||||||
|
embed-resource = "1.6"
|
||||||
anyhow = "1.0.27"
|
anyhow = "1.0.27"
|
||||||
bincode = "1.2.1"
|
bincode = "1.2.1"
|
||||||
fs-err = "2.3.0"
|
fs-err = "2.3.0"
|
||||||
@@ -98,8 +109,8 @@ rojo-insta-ext = { path = "rojo-insta-ext" }
|
|||||||
criterion = "0.3"
|
criterion = "0.3"
|
||||||
insta = { version = "1.3.0", features = ["redactions"] }
|
insta = { version = "1.3.0", features = ["redactions"] }
|
||||||
lazy_static = "1.2"
|
lazy_static = "1.2"
|
||||||
paste = "0.1"
|
paste = "1.0.5"
|
||||||
pretty_assertions = "0.6.1"
|
pretty_assertions = "0.7.2"
|
||||||
serde_yaml = "0.8.9"
|
serde_yaml = "0.8.9"
|
||||||
tempfile = "3.0"
|
tempfile = "3.0"
|
||||||
walkdir = "2.1"
|
walkdir = "2.1"
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -1,21 +1,13 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://rojo.space">
|
<a href="https://rojo.space"><img src="assets/logo-512.png" alt="Rojo" height="217" /></a>
|
||||||
<img src="assets/logo-512.png" alt="Rojo" height="217" />
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div> </div>
|
<div> </div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://github.com/rojo-rbx/rojo/actions">
|
<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>
|
||||||
<img src="https://github.com/rojo-rbx/rojo/workflows/CI/badge.svg" alt="Actions status" />
|
<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>
|
<a href="https://rojo.space/docs"><img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" /></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>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
@@ -48,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
|
|||||||
|
|
||||||
Pull requests are welcome!
|
Pull requests are welcome!
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||||
4
build.rs
4
build.rs
@@ -73,5 +73,9 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
|
|
||||||
bincode::serialize_into(out_file, &snapshot)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
2
build/windows/rojo-manifest.rc
Normal file
2
build/windows/rojo-manifest.rc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#define RT_MANIFEST 24
|
||||||
|
1 RT_MANIFEST "rojo.manifest"
|
||||||
8
build/windows/rojo.manifest
Normal file
8
build/windows/rojo.manifest
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?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>
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
[tools]
|
[tools]
|
||||||
rojo = { source = "rojo-rbx/rojo", version = "6.0.0-rc.3" }
|
rojo = { source = "rojo-rbx/rojo", version = "6.1.0" }
|
||||||
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }
|
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
## Unreleased Changes
|
## Unreleased Changes
|
||||||
|
|
||||||
|
## 0.2.0 (2021-08-23)
|
||||||
|
* Updated to `crossbeam-channel` 0.5.1.
|
||||||
|
|
||||||
## 0.1.3 (2020-11-19)
|
## 0.1.3 (2020-11-19)
|
||||||
* Added `set_watch_enabled` to `Vfs` and `VfsLock` to allow turning off file watching.
|
* Added `set_watch_enabled` to `Vfs` and `VfsLock` to allow turning off file watching.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "memofs"
|
name = "memofs"
|
||||||
description = "Virtual filesystem with configurable backends."
|
description = "Virtual filesystem with configurable backends."
|
||||||
version = "0.1.3"
|
version = "0.2.0"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
crossbeam-channel = "0.4.0"
|
crossbeam-channel = "0.5.1"
|
||||||
fs-err = "2.3.0"
|
fs-err = "2.3.0"
|
||||||
notify = "4.0.15"
|
notify = "4.0.15"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
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",
|
|
||||||
}
|
|
||||||
457
plugin/rbx_dom_lua/EncodedValue.lua
Normal file
457
plugin/rbx_dom_lua/EncodedValue.lua
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
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
|
||||||
72
plugin/rbx_dom_lua/EncodedValue.spec.lua
Normal file
72
plugin/rbx_dom_lua/EncodedValue.spec.lua
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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
|
||||||
@@ -21,7 +21,7 @@ end
|
|||||||
|
|
||||||
function PropertyDescriptor.fromRaw(data, className, propertyName)
|
function PropertyDescriptor.fromRaw(data, className, propertyName)
|
||||||
return setmetatable({
|
return setmetatable({
|
||||||
scriptability = data.scriptability,
|
scriptability = data.Scriptability,
|
||||||
className = className,
|
className = className,
|
||||||
name = propertyName,
|
name = propertyName,
|
||||||
}, PropertyDescriptor)
|
}, PropertyDescriptor)
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# rbx_dom_lua
|
|
||||||
Roblox Lua implementation of rbx-dom mechanisms, intended to work with rbx_dom_weak and friends.
|
|
||||||
316
plugin/rbx_dom_lua/allValues.json
Normal file
316
plugin/rbx_dom_lua/allValues.json
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
45553
plugin/rbx_dom_lua/database.json
Normal file
45553
plugin/rbx_dom_lua/database.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
local ReflectionDatabase = require(script.ReflectionDatabase)
|
local database = require(script.database)
|
||||||
local Error = require(script.Error)
|
local Error = require(script.Error)
|
||||||
local PropertyDescriptor = require(script.PropertyDescriptor)
|
local PropertyDescriptor = require(script.PropertyDescriptor)
|
||||||
|
|
||||||
@@ -6,29 +6,31 @@ local function findCanonicalPropertyDescriptor(className, propertyName)
|
|||||||
local currentClassName = className
|
local currentClassName = className
|
||||||
|
|
||||||
repeat
|
repeat
|
||||||
local currentClass = ReflectionDatabase.classes[currentClassName]
|
local currentClass = database.Classes[currentClassName]
|
||||||
|
|
||||||
if currentClass == nil then
|
if currentClass == nil then
|
||||||
return currentClass
|
return currentClass
|
||||||
end
|
end
|
||||||
|
|
||||||
local propertyData = currentClass.properties[propertyName]
|
local propertyData = currentClass.Properties[propertyName]
|
||||||
if propertyData ~= nil then
|
if propertyData ~= nil then
|
||||||
if propertyData.isCanonical then
|
local canonicalData = propertyData.Kind.Canonical
|
||||||
|
if canonicalData ~= nil then
|
||||||
return PropertyDescriptor.fromRaw(propertyData, currentClassName, propertyName)
|
return PropertyDescriptor.fromRaw(propertyData, currentClassName, propertyName)
|
||||||
end
|
end
|
||||||
|
|
||||||
if propertyData.canonicalName ~= nil then
|
local aliasData = propertyData.Kind.Alias
|
||||||
|
if aliasData ~= nil then
|
||||||
return PropertyDescriptor.fromRaw(
|
return PropertyDescriptor.fromRaw(
|
||||||
currentClass.properties[propertyData.canonicalName],
|
currentClass.Properties[aliasData.AliasFor],
|
||||||
currentClassName,
|
currentClassName,
|
||||||
propertyData.canonicalName)
|
aliasData.AliasFor)
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
currentClassName = currentClass.superclass
|
currentClassName = currentClass.Superclass
|
||||||
until currentClassName == nil
|
until currentClassName == nil
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -64,4 +66,4 @@ return {
|
|||||||
findCanonicalPropertyDescriptor = findCanonicalPropertyDescriptor,
|
findCanonicalPropertyDescriptor = findCanonicalPropertyDescriptor,
|
||||||
Error = Error,
|
Error = Error,
|
||||||
EncodedValue = require(script.EncodedValue),
|
EncodedValue = require(script.EncodedValue),
|
||||||
}
|
}
|
||||||
@@ -1,242 +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 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
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
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
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
|||||||
return {
|
|
||||||
classes = require(script.classes)
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
|
|
||||||
local LIB_ROOT = ReplicatedStorage.RbxDom
|
|
||||||
|
|
||||||
local TestEZ = require(ReplicatedStorage.TestEZ)
|
|
||||||
|
|
||||||
TestEZ.TestBootstrap:run({LIB_ROOT})
|
|
||||||
@@ -205,7 +205,7 @@ function SettingsPage:render()
|
|||||||
TwoWaySync = e(Setting, {
|
TwoWaySync = e(Setting, {
|
||||||
id = "twoWaySync",
|
id = "twoWaySync",
|
||||||
name = "Two-Way Sync",
|
name = "Two-Way Sync",
|
||||||
description = "Editing files in Studio will sync them into the filesystem",
|
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 2,
|
layoutOrder = 2,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
|
|||||||
return strict("Config", {
|
return strict("Config", {
|
||||||
isDevBuild = isDevBuild,
|
isDevBuild = isDevBuild,
|
||||||
codename = "Epiphany",
|
codename = "Epiphany",
|
||||||
version = {6, 0, 0},
|
version = {7, 0, 0, "-rc.1"},
|
||||||
expectedServerVersionString = "6.0 or newer",
|
expectedServerVersionString = "7.0 or newer",
|
||||||
protocolVersion = 3,
|
protocolVersion = 4,
|
||||||
defaultHost = "localhost",
|
defaultHost = "localhost",
|
||||||
defaultPort = 34872,
|
defaultPort = 34872,
|
||||||
})
|
})
|
||||||
@@ -146,8 +146,7 @@ return function()
|
|||||||
id = "VALUE",
|
id = "VALUE",
|
||||||
changedProperties = {
|
changedProperties = {
|
||||||
Value = {
|
Value = {
|
||||||
Type = "String",
|
String = "WORLD",
|
||||||
Value = "WORLD",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -176,8 +175,7 @@ return function()
|
|||||||
changedClassName = "StringValue",
|
changedClassName = "StringValue",
|
||||||
changedProperties = {
|
changedProperties = {
|
||||||
Value = {
|
Value = {
|
||||||
Type = "String",
|
String = "I am Root",
|
||||||
Value = "I am Root",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,29 +6,31 @@
|
|||||||
local RbxDom = require(script.Parent.Parent.Parent.RbxDom)
|
local RbxDom = require(script.Parent.Parent.Parent.RbxDom)
|
||||||
local Error = require(script.Parent.Error)
|
local Error = require(script.Parent.Error)
|
||||||
|
|
||||||
local function decodeValue(virtualValue, instanceMap)
|
local function decodeValue(encodedValue, instanceMap)
|
||||||
|
local ty, value = next(encodedValue)
|
||||||
|
|
||||||
-- Refs are represented as IDs in the same space that Rojo's protocol uses.
|
-- Refs are represented as IDs in the same space that Rojo's protocol uses.
|
||||||
if virtualValue.Type == "Ref" then
|
if ty == "Ref" then
|
||||||
if virtualValue.Value == nil then
|
if value == "00000000000000000000000000000000" then
|
||||||
return true, nil
|
return true, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local instance = instanceMap.fromIds[virtualValue.Value]
|
local instance = instanceMap.fromIds[value]
|
||||||
|
|
||||||
if instance ~= nil then
|
if instance ~= nil then
|
||||||
return true, instance
|
return true, instance
|
||||||
else
|
else
|
||||||
return false, Error.new(Error.RefDidNotExist, {
|
return false, Error.new(Error.RefDidNotExist, {
|
||||||
virtualValue = virtualValue,
|
encodedValue = encodedValue,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local ok, decodedValue = RbxDom.EncodedValue.decode(virtualValue)
|
local ok, decodedValue = RbxDom.EncodedValue.decode(encodedValue)
|
||||||
|
|
||||||
if not ok then
|
if not ok then
|
||||||
return false, Error.new(Error.CannotDecodeValue, {
|
return false, Error.new(Error.CannotDecodeValue, {
|
||||||
virtualValue = virtualValue,
|
encodedValue = encodedValue,
|
||||||
innerError = decodedValue,
|
innerError = decodedValue,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ local function diff(instanceMap, virtualInstances, rootId)
|
|||||||
changedProperties[propertyName] = virtualValue
|
changedProperties[propertyName] = virtualValue
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
Log.warn("Failed to decode property of type {}", virtualValue.Type)
|
-- 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)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
local err = existingValueOrErr
|
local err = existingValueOrErr
|
||||||
|
|||||||
@@ -80,8 +80,7 @@ return function()
|
|||||||
Name = "Value",
|
Name = "Value",
|
||||||
Properties = {
|
Properties = {
|
||||||
Value = {
|
Value = {
|
||||||
Type = "String",
|
String = "Hello, world!",
|
||||||
Value = "Hello, world!",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Children = {},
|
Children = {},
|
||||||
@@ -107,8 +106,9 @@ return function()
|
|||||||
|
|
||||||
local patchProperty = update.changedProperties["Value"]
|
local patchProperty = update.changedProperties["Value"]
|
||||||
expect(patchProperty).to.be.a("table")
|
expect(patchProperty).to.be.a("table")
|
||||||
expect(patchProperty.Type).to.equal("String")
|
local ty, value = next(patchProperty)
|
||||||
expect(patchProperty.Value).to.equal("Hello, world!")
|
expect(ty).to.equal("String")
|
||||||
|
expect(value).to.equal("Hello, world!")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("should generate an empty patch if no properties changed", function()
|
it("should generate an empty patch if no properties changed", function()
|
||||||
@@ -119,8 +119,7 @@ return function()
|
|||||||
Name = "Value",
|
Name = "Value",
|
||||||
Properties = {
|
Properties = {
|
||||||
Value = {
|
Value = {
|
||||||
Type = "String",
|
String = "Hello, world!",
|
||||||
Value = "Hello, world!",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Children = {},
|
Children = {},
|
||||||
@@ -145,8 +144,7 @@ return function()
|
|||||||
Name = "Folder",
|
Name = "Folder",
|
||||||
Properties = {
|
Properties = {
|
||||||
FAKE_PROPERTY = {
|
FAKE_PROPERTY = {
|
||||||
Type = "String",
|
String = "Hello, world!",
|
||||||
Value = "Hello, world!",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Children = {},
|
Children = {},
|
||||||
@@ -183,8 +181,7 @@ return function()
|
|||||||
-- heat_xml is a serialization-only property that is not
|
-- heat_xml is a serialization-only property that is not
|
||||||
-- exposed to Lua.
|
-- exposed to Lua.
|
||||||
heat_xml = {
|
heat_xml = {
|
||||||
Type = "Float32",
|
Float32 = 5,
|
||||||
Value = 5,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Children = {},
|
Children = {},
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function reifyInner(instanceMap, virtualInstances, id, parentInstance, unapplied
|
|||||||
for propertyName, virtualValue in pairs(virtualInstance.Properties) do
|
for propertyName, virtualValue in pairs(virtualInstance.Properties) do
|
||||||
-- Because refs may refer to instances that we haven't constructed yet,
|
-- Because refs may refer to instances that we haven't constructed yet,
|
||||||
-- we defer applying any ref properties until all instances are created.
|
-- we defer applying any ref properties until all instances are created.
|
||||||
if virtualValue.Type == "Ref" then
|
if next(virtualValue) == "Ref" then
|
||||||
table.insert(deferredRefs, {
|
table.insert(deferredRefs, {
|
||||||
id = id,
|
id = id,
|
||||||
instance = instance,
|
instance = instance,
|
||||||
@@ -136,23 +136,23 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
|||||||
end
|
end
|
||||||
|
|
||||||
for _, entry in ipairs(deferredRefs) do
|
for _, entry in ipairs(deferredRefs) do
|
||||||
local virtualValue = entry.virtualValue
|
local _, refId = next(entry.virtualValue)
|
||||||
|
|
||||||
if virtualValue.Value == nil then
|
if refId == nil then
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
local targetInstance = instanceMap.fromIds[virtualValue.Value]
|
local targetInstance = instanceMap.fromIds[refId]
|
||||||
if targetInstance == nil then
|
if targetInstance == nil then
|
||||||
markFailed(entry.id, entry.propertyName, virtualValue)
|
markFailed(entry.id, entry.propertyName, entry.virtualValue)
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
local ok = setProperty(entry.instance, entry.propertyName, targetInstance)
|
local ok = setProperty(entry.instance, entry.propertyName, targetInstance)
|
||||||
if not ok then
|
if not ok then
|
||||||
markFailed(entry.id, entry.propertyName, virtualValue)
|
markFailed(entry.id, entry.propertyName, entry.virtualValue)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return reify
|
return reify
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ return function()
|
|||||||
Name = "Spaghetti",
|
Name = "Spaghetti",
|
||||||
Properties = {
|
Properties = {
|
||||||
Value = {
|
Value = {
|
||||||
Type = "String",
|
String = "Hello, world!",
|
||||||
Value = "Hello, world!",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Children = {},
|
Children = {},
|
||||||
@@ -191,8 +190,7 @@ return function()
|
|||||||
Name = "Child",
|
Name = "Child",
|
||||||
Properties = {
|
Properties = {
|
||||||
Value = {
|
Value = {
|
||||||
Type = "Ref",
|
Ref = "ROOT",
|
||||||
Value = "ROOT",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Children = {},
|
Children = {},
|
||||||
@@ -219,8 +217,7 @@ return function()
|
|||||||
Name = "Root",
|
Name = "Root",
|
||||||
Properties = {
|
Properties = {
|
||||||
Value = {
|
Value = {
|
||||||
Type = "Ref",
|
Ref = "EXISTING",
|
||||||
Value = "EXISTING",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Children = {},
|
Children = {},
|
||||||
@@ -258,8 +255,7 @@ return function()
|
|||||||
Name = "Child A",
|
Name = "Child A",
|
||||||
Properties = {
|
Properties = {
|
||||||
Value = {
|
Value = {
|
||||||
Type = "Ref",
|
Ref = "Child B",
|
||||||
Value = "Child B",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Children = {},
|
Children = {},
|
||||||
@@ -298,8 +294,7 @@ return function()
|
|||||||
Name = "Root",
|
Name = "Root",
|
||||||
Properties = {
|
Properties = {
|
||||||
Value = {
|
Value = {
|
||||||
Type = "Ref",
|
Ref = "CHILD",
|
||||||
Value = "CHILD",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Children = {"CHILD"},
|
Children = {"CHILD"},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
local StudioService = game:GetService("StudioService")
|
local StudioService = game:GetService("StudioService")
|
||||||
|
local RunService = game:GetService("RunService")
|
||||||
|
|
||||||
local Log = require(script.Parent.Parent.Log)
|
local Log = require(script.Parent.Parent.Log)
|
||||||
local Fmt = require(script.Parent.Parent.Fmt)
|
local Fmt = require(script.Parent.Parent.Fmt)
|
||||||
@@ -111,6 +112,7 @@ function ServeSession:start()
|
|||||||
self.__apiContext:connect()
|
self.__apiContext:connect()
|
||||||
:andThen(function(serverInfo)
|
:andThen(function(serverInfo)
|
||||||
self:__setStatus(Status.Connected, serverInfo.projectName)
|
self:__setStatus(Status.Connected, serverInfo.projectName)
|
||||||
|
self:__applyGameAndPlaceId(serverInfo)
|
||||||
|
|
||||||
local rootInstanceId = serverInfo.rootInstanceId
|
local rootInstanceId = serverInfo.rootInstanceId
|
||||||
|
|
||||||
@@ -128,6 +130,16 @@ function ServeSession:stop()
|
|||||||
self:__stopInternal()
|
self:__stopInternal()
|
||||||
end
|
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)
|
function ServeSession:__onActiveScriptChanged(activeScript)
|
||||||
if not self.__openScriptsExternally then
|
if not self.__openScriptsExternally then
|
||||||
Log.trace("Not opening script {} because feature not enabled.", activeScript)
|
Log.trace("Not opening script {} because feature not enabled.", activeScript)
|
||||||
@@ -150,10 +162,18 @@ function ServeSession:__onActiveScriptChanged(activeScript)
|
|||||||
|
|
||||||
Log.debug("Trying to open script {} externally...", activeScript)
|
Log.debug("Trying to open script {} externally...", activeScript)
|
||||||
|
|
||||||
-- Force-close the script inside Studio
|
-- Force-close the script inside Studio... with a small delay in the middle
|
||||||
local existingParent = activeScript.Parent
|
-- to prevent Studio from crashing.
|
||||||
activeScript.Parent = nil
|
spawn(function()
|
||||||
activeScript.Parent = existingParent
|
local existingParent = activeScript.Parent
|
||||||
|
activeScript.Parent = nil
|
||||||
|
|
||||||
|
for i = 1, 3 do
|
||||||
|
RunService.Heartbeat:Wait()
|
||||||
|
end
|
||||||
|
|
||||||
|
activeScript.Parent = existingParent
|
||||||
|
end)
|
||||||
|
|
||||||
-- Notify the Rojo server to open this script
|
-- Notify the Rojo server to open this script
|
||||||
self.__apiContext:open(scriptId)
|
self.__apiContext:open(scriptId)
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ local strict = require(script.Parent.strict)
|
|||||||
|
|
||||||
local RbxId = t.string
|
local RbxId = t.string
|
||||||
|
|
||||||
local ApiValue = t.interface({
|
local ApiValue = t.keys(t.string)
|
||||||
Type = t.string,
|
|
||||||
Value = t.optional(t.any),
|
|
||||||
})
|
|
||||||
|
|
||||||
local ApiInstanceMetadata = t.interface({
|
local ApiInstanceMetadata = t.interface({
|
||||||
ignoreUnknownInstances = t.optional(t.boolean),
|
ignoreUnknownInstances = t.optional(t.boolean),
|
||||||
@@ -96,4 +93,4 @@ return strict("Types", {
|
|||||||
VirtualInstance = ApiInstance,
|
VirtualInstance = ApiInstance,
|
||||||
VirtualMetadata = ApiInstanceMetadata,
|
VirtualMetadata = ApiInstanceMetadata,
|
||||||
VirtualValue = ApiValue,
|
VirtualValue = ApiValue,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ expression: contents
|
|||||||
<Item class="IntValue" referent="1">
|
<Item class="IntValue" referent="1">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">simple-model</string>
|
<string name="Name">simple-model</string>
|
||||||
<int name="Value">5</int>
|
<int64 name="Value">5</int64>
|
||||||
</Properties>
|
</Properties>
|
||||||
<Item class="Folder" referent="2">
|
<Item class="Folder" referent="2">
|
||||||
<Properties>
|
<Properties>
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
43
rojo-test/build-tests/unresolved_values/default.project.json
Normal file
43
rojo-test/build-tests/unresolved_values/default.project.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ instances:
|
|||||||
Metadata:
|
Metadata:
|
||||||
ignoreUnknownInstances: false
|
ignoreUnknownInstances: false
|
||||||
Name: add_folder
|
Name: add_folder
|
||||||
Parent: ~
|
Parent: "00000000000000000000000000000000"
|
||||||
Properties: {}
|
Properties: {}
|
||||||
id-3:
|
id-3:
|
||||||
Children: []
|
Children: []
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ instances:
|
|||||||
Metadata:
|
Metadata:
|
||||||
ignoreUnknownInstances: false
|
ignoreUnknownInstances: false
|
||||||
Name: add_folder
|
Name: add_folder
|
||||||
Parent: ~
|
Parent: "00000000000000000000000000000000"
|
||||||
Properties: {}
|
Properties: {}
|
||||||
messageCursor: 0
|
messageCursor: 0
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: redactions.redacted_yaml(info)
|
expression: redactions.redacted_yaml(info)
|
||||||
|
|
||||||
---
|
---
|
||||||
expectedPlaceIds: ~
|
expectedPlaceIds: ~
|
||||||
|
gameId: ~
|
||||||
|
placeId: ~
|
||||||
projectName: add_folder
|
projectName: add_folder
|
||||||
protocolVersion: 3
|
protocolVersion: 4
|
||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||||
|
|
||||||
---
|
---
|
||||||
instances:
|
instances:
|
||||||
id-2:
|
id-2:
|
||||||
@@ -10,10 +11,10 @@ instances:
|
|||||||
Metadata:
|
Metadata:
|
||||||
ignoreUnknownInstances: false
|
ignoreUnknownInstances: false
|
||||||
Name: edit_init
|
Name: edit_init
|
||||||
Parent: ~
|
Parent: "00000000000000000000000000000000"
|
||||||
Properties:
|
Properties:
|
||||||
Source:
|
Source:
|
||||||
Type: String
|
String: "-- Edited contents"
|
||||||
Value: "-- Edited contents"
|
|
||||||
messageCursor: 1
|
messageCursor: 1
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||||
|
|
||||||
---
|
---
|
||||||
instances:
|
instances:
|
||||||
id-2:
|
id-2:
|
||||||
@@ -10,10 +11,10 @@ instances:
|
|||||||
Metadata:
|
Metadata:
|
||||||
ignoreUnknownInstances: false
|
ignoreUnknownInstances: false
|
||||||
Name: edit_init
|
Name: edit_init
|
||||||
Parent: ~
|
Parent: "00000000000000000000000000000000"
|
||||||
Properties:
|
Properties:
|
||||||
Source:
|
Source:
|
||||||
Type: String
|
String: "-- Original contents"
|
||||||
Value: "-- Original contents"
|
|
||||||
messageCursor: 0
|
messageCursor: 0
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: redactions.redacted_yaml(info)
|
expression: redactions.redacted_yaml(info)
|
||||||
|
|
||||||
---
|
---
|
||||||
expectedPlaceIds: ~
|
expectedPlaceIds: ~
|
||||||
|
gameId: ~
|
||||||
|
placeId: ~
|
||||||
projectName: edit_init
|
projectName: edit_init
|
||||||
protocolVersion: 3
|
protocolVersion: 4
|
||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||||
|
|
||||||
---
|
---
|
||||||
messageCursor: 1
|
messageCursor: 1
|
||||||
messages:
|
messages:
|
||||||
@@ -12,7 +13,7 @@ messages:
|
|||||||
changedName: ~
|
changedName: ~
|
||||||
changedProperties:
|
changedProperties:
|
||||||
Source:
|
Source:
|
||||||
Type: String
|
String: "-- Edited contents"
|
||||||
Value: "-- Edited contents"
|
|
||||||
id: id-2
|
id: id-2
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ instances:
|
|||||||
Metadata:
|
Metadata:
|
||||||
ignoreUnknownInstances: true
|
ignoreUnknownInstances: true
|
||||||
Name: empty
|
Name: empty
|
||||||
Parent: ~
|
Parent: "00000000000000000000000000000000"
|
||||||
Properties: {}
|
Properties: {}
|
||||||
messageCursor: 0
|
messageCursor: 0
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: redactions.redacted_yaml(info)
|
expression: redactions.redacted_yaml(info)
|
||||||
|
|
||||||
---
|
---
|
||||||
expectedPlaceIds: ~
|
expectedPlaceIds: ~
|
||||||
|
gameId: ~
|
||||||
|
placeId: ~
|
||||||
projectName: empty
|
projectName: empty
|
||||||
protocolVersion: 3
|
protocolVersion: 4
|
||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||||
|
|
||||||
---
|
---
|
||||||
instances:
|
instances:
|
||||||
id-10:
|
id-10:
|
||||||
@@ -13,8 +14,7 @@ instances:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #6"
|
||||||
Value: "File #6"
|
|
||||||
id-11:
|
id-11:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -25,8 +25,7 @@ instances:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #7"
|
||||||
Value: "File #7"
|
|
||||||
id-12:
|
id-12:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -37,8 +36,7 @@ instances:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #8"
|
||||||
Value: "File #8"
|
|
||||||
id-13:
|
id-13:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -49,8 +47,7 @@ instances:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #9"
|
||||||
Value: "File #9"
|
|
||||||
id-2:
|
id-2:
|
||||||
Children:
|
Children:
|
||||||
- id-3
|
- id-3
|
||||||
@@ -59,7 +56,7 @@ instances:
|
|||||||
Metadata:
|
Metadata:
|
||||||
ignoreUnknownInstances: false
|
ignoreUnknownInstances: false
|
||||||
Name: move_folder_of_stuff
|
Name: move_folder_of_stuff
|
||||||
Parent: ~
|
Parent: "00000000000000000000000000000000"
|
||||||
Properties: {}
|
Properties: {}
|
||||||
id-3:
|
id-3:
|
||||||
Children:
|
Children:
|
||||||
@@ -90,8 +87,7 @@ instances:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #0"
|
||||||
Value: "File #0"
|
|
||||||
id-5:
|
id-5:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -102,8 +98,7 @@ instances:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #1"
|
||||||
Value: "File #1"
|
|
||||||
id-6:
|
id-6:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -114,8 +109,7 @@ instances:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #2"
|
||||||
Value: "File #2"
|
|
||||||
id-7:
|
id-7:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -126,8 +120,7 @@ instances:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #3"
|
||||||
Value: "File #3"
|
|
||||||
id-8:
|
id-8:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -138,8 +131,7 @@ instances:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #4"
|
||||||
Value: "File #4"
|
|
||||||
id-9:
|
id-9:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -150,7 +142,7 @@ instances:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #5"
|
||||||
Value: "File #5"
|
|
||||||
messageCursor: 1
|
messageCursor: 1
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ instances:
|
|||||||
Metadata:
|
Metadata:
|
||||||
ignoreUnknownInstances: false
|
ignoreUnknownInstances: false
|
||||||
Name: move_folder_of_stuff
|
Name: move_folder_of_stuff
|
||||||
Parent: ~
|
Parent: "00000000000000000000000000000000"
|
||||||
Properties: {}
|
Properties: {}
|
||||||
messageCursor: 0
|
messageCursor: 0
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: redactions.redacted_yaml(info)
|
expression: redactions.redacted_yaml(info)
|
||||||
|
|
||||||
---
|
---
|
||||||
expectedPlaceIds: ~
|
expectedPlaceIds: ~
|
||||||
|
gameId: ~
|
||||||
|
placeId: ~
|
||||||
projectName: move_folder_of_stuff
|
projectName: move_folder_of_stuff
|
||||||
protocolVersion: 3
|
protocolVersion: 4
|
||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||||
|
|
||||||
---
|
---
|
||||||
messageCursor: 1
|
messageCursor: 1
|
||||||
messages:
|
messages:
|
||||||
@@ -15,8 +16,7 @@ messages:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #6"
|
||||||
Value: "File #6"
|
|
||||||
id-11:
|
id-11:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -27,8 +27,7 @@ messages:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #7"
|
||||||
Value: "File #7"
|
|
||||||
id-12:
|
id-12:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -39,8 +38,7 @@ messages:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #8"
|
||||||
Value: "File #8"
|
|
||||||
id-13:
|
id-13:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -51,8 +49,7 @@ messages:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #9"
|
||||||
Value: "File #9"
|
|
||||||
id-3:
|
id-3:
|
||||||
Children:
|
Children:
|
||||||
- id-4
|
- id-4
|
||||||
@@ -82,8 +79,7 @@ messages:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #0"
|
||||||
Value: "File #0"
|
|
||||||
id-5:
|
id-5:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -94,8 +90,7 @@ messages:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #1"
|
||||||
Value: "File #1"
|
|
||||||
id-6:
|
id-6:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -106,8 +101,7 @@ messages:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #2"
|
||||||
Value: "File #2"
|
|
||||||
id-7:
|
id-7:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -118,8 +112,7 @@ messages:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #3"
|
||||||
Value: "File #3"
|
|
||||||
id-8:
|
id-8:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -130,8 +123,7 @@ messages:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #4"
|
||||||
Value: "File #4"
|
|
||||||
id-9:
|
id-9:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: StringValue
|
ClassName: StringValue
|
||||||
@@ -142,8 +134,8 @@ messages:
|
|||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: "File #5"
|
||||||
Value: "File #5"
|
|
||||||
removed: []
|
removed: []
|
||||||
updated: []
|
updated: []
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ instances:
|
|||||||
Metadata:
|
Metadata:
|
||||||
ignoreUnknownInstances: false
|
ignoreUnknownInstances: false
|
||||||
Name: remove_file
|
Name: remove_file
|
||||||
Parent: ~
|
Parent: "00000000000000000000000000000000"
|
||||||
Properties: {}
|
Properties: {}
|
||||||
messageCursor: 1
|
messageCursor: 1
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||||
|
|
||||||
---
|
---
|
||||||
instances:
|
instances:
|
||||||
id-2:
|
id-2:
|
||||||
@@ -11,7 +12,7 @@ instances:
|
|||||||
Metadata:
|
Metadata:
|
||||||
ignoreUnknownInstances: false
|
ignoreUnknownInstances: false
|
||||||
Name: remove_file
|
Name: remove_file
|
||||||
Parent: ~
|
Parent: "00000000000000000000000000000000"
|
||||||
Properties: {}
|
Properties: {}
|
||||||
id-3:
|
id-3:
|
||||||
Children: []
|
Children: []
|
||||||
@@ -23,7 +24,7 @@ instances:
|
|||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties:
|
Properties:
|
||||||
Value:
|
Value:
|
||||||
Type: String
|
String: This file will be removed!
|
||||||
Value: This file will be removed!
|
|
||||||
messageCursor: 0
|
messageCursor: 0
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: redactions.redacted_yaml(info)
|
expression: redactions.redacted_yaml(info)
|
||||||
|
|
||||||
---
|
---
|
||||||
expectedPlaceIds: ~
|
expectedPlaceIds: ~
|
||||||
|
gameId: ~
|
||||||
|
placeId: ~
|
||||||
projectName: remove_file
|
projectName: remove_file
|
||||||
protocolVersion: 3
|
protocolVersion: 4
|
||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||||
|
|
||||||
---
|
---
|
||||||
instances:
|
instances:
|
||||||
id-2:
|
id-2:
|
||||||
@@ -12,7 +13,7 @@ instances:
|
|||||||
Metadata:
|
Metadata:
|
||||||
ignoreUnknownInstances: false
|
ignoreUnknownInstances: false
|
||||||
Name: scripts
|
Name: scripts
|
||||||
Parent: ~
|
Parent: "00000000000000000000000000000000"
|
||||||
Properties: {}
|
Properties: {}
|
||||||
id-3:
|
id-3:
|
||||||
Children: []
|
Children: []
|
||||||
@@ -24,8 +25,7 @@ instances:
|
|||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties:
|
Properties:
|
||||||
Source:
|
Source:
|
||||||
Type: String
|
String: "-- Hello, from bar!"
|
||||||
Value: "-- Hello, from bar!"
|
|
||||||
id-4:
|
id-4:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: ModuleScript
|
ClassName: ModuleScript
|
||||||
@@ -36,7 +36,7 @@ instances:
|
|||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties:
|
Properties:
|
||||||
Source:
|
Source:
|
||||||
Type: String
|
String: Updated foo!
|
||||||
Value: Updated foo!
|
|
||||||
messageCursor: 1
|
messageCursor: 1
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||||
|
|
||||||
---
|
---
|
||||||
instances:
|
instances:
|
||||||
id-2:
|
id-2:
|
||||||
@@ -12,7 +13,7 @@ instances:
|
|||||||
Metadata:
|
Metadata:
|
||||||
ignoreUnknownInstances: false
|
ignoreUnknownInstances: false
|
||||||
Name: scripts
|
Name: scripts
|
||||||
Parent: ~
|
Parent: "00000000000000000000000000000000"
|
||||||
Properties: {}
|
Properties: {}
|
||||||
id-3:
|
id-3:
|
||||||
Children: []
|
Children: []
|
||||||
@@ -24,8 +25,7 @@ instances:
|
|||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties:
|
Properties:
|
||||||
Source:
|
Source:
|
||||||
Type: String
|
String: "-- Hello, from bar!"
|
||||||
Value: "-- Hello, from bar!"
|
|
||||||
id-4:
|
id-4:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: ModuleScript
|
ClassName: ModuleScript
|
||||||
@@ -36,7 +36,7 @@ instances:
|
|||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties:
|
Properties:
|
||||||
Source:
|
Source:
|
||||||
Type: String
|
String: "-- Hello, from foo!"
|
||||||
Value: "-- Hello, from foo!"
|
|
||||||
messageCursor: 0
|
messageCursor: 0
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: redactions.redacted_yaml(info)
|
expression: redactions.redacted_yaml(info)
|
||||||
|
|
||||||
---
|
---
|
||||||
expectedPlaceIds: ~
|
expectedPlaceIds: ~
|
||||||
|
gameId: ~
|
||||||
|
placeId: ~
|
||||||
projectName: scripts
|
projectName: scripts
|
||||||
protocolVersion: 3
|
protocolVersion: 4
|
||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||||
|
|
||||||
---
|
---
|
||||||
messageCursor: 1
|
messageCursor: 1
|
||||||
messages:
|
messages:
|
||||||
@@ -12,7 +13,7 @@ messages:
|
|||||||
changedName: ~
|
changedName: ~
|
||||||
changedProperties:
|
changedProperties:
|
||||||
Source:
|
Source:
|
||||||
Type: String
|
String: Updated foo!
|
||||||
Value: Updated foo!
|
|
||||||
id: id-4
|
id: id-4
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "rbx_dom_lua",
|
"name": "empty_folder",
|
||||||
"tree": {
|
"tree": {
|
||||||
"$path": "src"
|
"$path": "src"
|
||||||
}
|
}
|
||||||
17
src/bin.rs
17
src/bin.rs
@@ -3,20 +3,7 @@ use std::{env, panic, process};
|
|||||||
use backtrace::Backtrace;
|
use backtrace::Backtrace;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
use librojo::cli::{self, GlobalOptions, Options, Subcommand};
|
use librojo::cli::Options;
|
||||||
|
|
||||||
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() {
|
fn main() {
|
||||||
panic::set_hook(Box::new(|panic_info| {
|
panic::set_hook(Box::new(|panic_info| {
|
||||||
@@ -81,7 +68,7 @@ fn main() {
|
|||||||
.write_style(options.global.color.into())
|
.write_style(options.global.color.into())
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
if let Err(err) = run(options.global, options.subcommand) {
|
if let Err(err) = options.run() {
|
||||||
log::error!("{:?}", err);
|
log::error!("{:?}", err);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ use std::{
|
|||||||
use crossbeam_channel::{select, Receiver, RecvError, Sender};
|
use crossbeam_channel::{select, Receiver, RecvError, Sender};
|
||||||
use jod_thread::JoinHandle;
|
use jod_thread::JoinHandle;
|
||||||
use memofs::{IoResultExt, Vfs, VfsEvent};
|
use memofs::{IoResultExt, Vfs, VfsEvent};
|
||||||
use rbx_dom_weak::{RbxId, RbxValue};
|
use rbx_dom_weak::types::{Ref, Variant};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::ErrorDisplay,
|
|
||||||
message_queue::MessageQueue,
|
message_queue::MessageQueue,
|
||||||
snapshot::{
|
snapshot::{
|
||||||
apply_patch_set, compute_patch_set, AppliedPatchSet, InstigatingSource, PatchSet, RojoTree,
|
apply_patch_set, compute_patch_set, AppliedPatchSet, InstigatingSource, PatchSet, RojoTree,
|
||||||
@@ -17,6 +16,9 @@ use crate::{
|
|||||||
snapshot_middleware::{snapshot_from_vfs, snapshot_project_node},
|
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
|
/// Owns the connection between Rojo's VFS and its DOM by holding onto another
|
||||||
/// thread that processes messages.
|
/// thread that processes messages.
|
||||||
///
|
///
|
||||||
@@ -180,7 +182,7 @@ impl JobThreadContext {
|
|||||||
InstigatingSource::Path(path) => fs::remove_file(path).unwrap(),
|
InstigatingSource::Path(path) => fs::remove_file(path).unwrap(),
|
||||||
InstigatingSource::ProjectNode(_, _, _, _) => {
|
InstigatingSource::ProjectNode(_, _, _, _) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Cannot remove instance {}, it's from a project file",
|
"Cannot remove instance {:?}, it's from a project file",
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -188,12 +190,12 @@ impl JobThreadContext {
|
|||||||
} else {
|
} else {
|
||||||
// TODO
|
// TODO
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Cannot remove instance {}, it is not an instigating source.",
|
"Cannot remove instance {:?}, it is not an instigating source.",
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::warn!("Cannot remove instance {}, it does not exist.", id);
|
log::warn!("Cannot remove instance {:?}, it does not exist.", id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +222,7 @@ impl JobThreadContext {
|
|||||||
{
|
{
|
||||||
match instigating_source {
|
match instigating_source {
|
||||||
InstigatingSource::Path(path) => {
|
InstigatingSource::Path(path) => {
|
||||||
if let Some(RbxValue::String { value }) = changed_value {
|
if let Some(Variant::String(value)) = changed_value {
|
||||||
fs::write(path, value).unwrap();
|
fs::write(path, value).unwrap();
|
||||||
} else {
|
} else {
|
||||||
log::warn!("Cannot change Source to non-string value.");
|
log::warn!("Cannot change Source to non-string value.");
|
||||||
@@ -228,14 +230,14 @@ impl JobThreadContext {
|
|||||||
}
|
}
|
||||||
InstigatingSource::ProjectNode(_, _, _, _) => {
|
InstigatingSource::ProjectNode(_, _, _, _) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Cannot remove instance {}, it's from a project file",
|
"Cannot remove instance {:?}, it's from a project file",
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Cannot update instance {}, it is not an instigating source.",
|
"Cannot update instance {:?}, it is not an instigating source.",
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -244,7 +246,7 @@ impl JobThreadContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::warn!("Cannot update instance {}, it does not exist.", id);
|
log::warn!("Cannot update instance {:?}, it does not exist.", id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +257,7 @@ impl JobThreadContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: RbxId) -> Option<AppliedPatchSet> {
|
fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<AppliedPatchSet> {
|
||||||
let metadata = tree
|
let metadata = tree
|
||||||
.get_metadata(id)
|
.get_metadata(id)
|
||||||
.expect("metadata missing for instance present in tree");
|
.expect("metadata missing for instance present in tree");
|
||||||
@@ -264,7 +266,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: RbxId) -> Optio
|
|||||||
Some(path) => path,
|
Some(path) => path,
|
||||||
None => {
|
None => {
|
||||||
log::error!(
|
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
|
id
|
||||||
);
|
);
|
||||||
log::error!("This is a bug. Please file an issue!");
|
log::error!("This is a bug. Please file an issue!");
|
||||||
@@ -313,7 +315,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: RbxId) -> Optio
|
|||||||
apply_patch_set(tree, patch_set)
|
apply_patch_set(tree, patch_set)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("Error processing filesystem change: {}", ErrorDisplay(err));
|
log::error!("Error processing filesystem change: {:?}", err);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
152
src/cli/build.rs
152
src/cli/build.rs
@@ -1,24 +1,88 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
|
||||||
io::{BufWriter, Write},
|
io::{BufWriter, Write},
|
||||||
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use fs_err::File;
|
||||||
use memofs::Vfs;
|
use memofs::Vfs;
|
||||||
use thiserror::Error;
|
use structopt::StructOpt;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
use crate::{cli::BuildCommand, serve_session::ServeSession, snapshot::RojoTree};
|
use crate::serve_session::ServeSession;
|
||||||
|
|
||||||
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum OutputKind {
|
enum OutputKind {
|
||||||
|
/// An XML model file.
|
||||||
Rbxmx,
|
Rbxmx,
|
||||||
|
|
||||||
|
/// An XML place file.
|
||||||
Rbxlx,
|
Rbxlx,
|
||||||
|
|
||||||
|
/// A binary model file.
|
||||||
Rbxm,
|
Rbxm,
|
||||||
|
|
||||||
|
/// A binary place file.
|
||||||
Rbxl,
|
Rbxl,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_output_kind(options: &BuildCommand) -> Option<OutputKind> {
|
fn detect_output_kind(output: &Path) -> Option<OutputKind> {
|
||||||
let extension = options.output.extension()?.to_str()?;
|
let extension = output.extension()?.to_str()?;
|
||||||
|
|
||||||
match extension {
|
match extension {
|
||||||
"rbxlx" => Some(OutputKind::Rbxlx),
|
"rbxlx" => Some(OutputKind::Rbxlx),
|
||||||
@@ -29,57 +93,33 @@ fn detect_output_kind(options: &BuildCommand) -> 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 {
|
fn xml_encode_config() -> rbx_xml::EncodeOptions {
|
||||||
rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
|
rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(options: BuildCommand) -> Result<(), anyhow::Error> {
|
fn write_model(
|
||||||
log::trace!("Constructing in-memory filesystem");
|
session: &ServeSession,
|
||||||
|
output: &Path,
|
||||||
let vfs = Vfs::new_default();
|
output_kind: OutputKind,
|
||||||
vfs.set_watch_enabled(options.watch);
|
) -> anyhow::Result<()> {
|
||||||
|
println!("Building project '{}'", session.project_name());
|
||||||
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();
|
let root_id = tree.get_root_id();
|
||||||
|
|
||||||
log::trace!("Opening output file for write");
|
log::trace!("Opening output file for write");
|
||||||
let file = File::create(&options.output)?;
|
let mut file = BufWriter::new(File::create(output)?);
|
||||||
let mut file = BufWriter::new(file);
|
|
||||||
|
|
||||||
match output_kind {
|
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 => {
|
OutputKind::Rbxmx => {
|
||||||
// Model files include the root instance of the tree and all its
|
// Model files include the root instance of the tree and all its
|
||||||
// descendants.
|
// descendants.
|
||||||
@@ -88,36 +128,22 @@ fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), anyhow::Er
|
|||||||
}
|
}
|
||||||
OutputKind::Rbxlx => {
|
OutputKind::Rbxlx => {
|
||||||
// Place files don't contain an entry for the DataModel, but our
|
// Place files don't contain an entry for the DataModel, but our
|
||||||
// RbxTree representation does.
|
// WeakDom representation does.
|
||||||
|
|
||||||
let root_instance = tree.get_instance(root_id).unwrap();
|
let root_instance = tree.get_instance(root_id).unwrap();
|
||||||
let top_level_ids = root_instance.children();
|
let top_level_ids = root_instance.children();
|
||||||
|
|
||||||
rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())?;
|
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()?;
|
file.flush()?;
|
||||||
|
|
||||||
let filename = options
|
let filename = output
|
||||||
.output
|
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|name| name.to_str())
|
.and_then(|name| name.to_str())
|
||||||
.unwrap_or("<invalid utf-8>");
|
.unwrap_or("<invalid utf-8>");
|
||||||
log::info!("Built project to {}", filename);
|
println!("Built project to {}", filename);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
pub fn doc() -> Result<(), anyhow::Error> {
|
use structopt::StructOpt;
|
||||||
opener::open("https://rojo.space/docs")?;
|
|
||||||
Ok(())
|
/// 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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/cli/fmt_project.rs
Normal file
29
src/cli/fmt_project.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/cli/init.rs
111
src/cli/init.rs
@@ -1,13 +1,14 @@
|
|||||||
use std::{
|
use std::io::{self, Write};
|
||||||
fs::{self, OpenOptions},
|
use std::path::{Path, PathBuf};
|
||||||
io::{self, Write},
|
use std::process::{Command, Stdio};
|
||||||
path::Path,
|
use std::str::FromStr;
|
||||||
process::{Command, Stdio},
|
|
||||||
};
|
|
||||||
|
|
||||||
use thiserror::Error;
|
use anyhow::{bail, format_err};
|
||||||
|
use fs_err as fs;
|
||||||
|
use fs_err::OpenOptions;
|
||||||
|
use structopt::StructOpt;
|
||||||
|
|
||||||
use crate::cli::{InitCommand, InitKind};
|
use super::resolve_path;
|
||||||
|
|
||||||
static MODEL_PROJECT: &str =
|
static MODEL_PROJECT: &str =
|
||||||
include_str!("../../assets/default-model-project/default.project.json");
|
include_str!("../../assets/default-model-project/default.project.json");
|
||||||
@@ -20,37 +21,71 @@ static PLACE_PROJECT: &str =
|
|||||||
static PLACE_README: &str = include_str!("../../assets/default-place-project/README.md");
|
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");
|
static PLACE_GIT_IGNORE: &str = include_str!("../../assets/default-place-project/gitignore.txt");
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
/// Initializes a new Rojo project.
|
||||||
enum Error {
|
#[derive(Debug, StructOpt)]
|
||||||
#[error("A project file named default.project.json already exists in this folder")]
|
pub struct InitCommand {
|
||||||
AlreadyExists,
|
/// Path to the place to create the project. Defaults to the current directory.
|
||||||
|
#[structopt(default_value = "")]
|
||||||
|
pub path: PathBuf,
|
||||||
|
|
||||||
#[error("git init failed")]
|
/// The kind of project to create, 'place' or 'model'. Defaults to place.
|
||||||
GitInit,
|
#[structopt(long, default_value = "place")]
|
||||||
|
pub kind: InitKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(options: InitCommand) -> Result<(), anyhow::Error> {
|
impl InitCommand {
|
||||||
let base_path = options.absolute_path();
|
pub fn run(self) -> anyhow::Result<()> {
|
||||||
fs::create_dir_all(&base_path)?;
|
let base_path = resolve_path(&self.path);
|
||||||
|
fs::create_dir_all(&base_path)?;
|
||||||
|
|
||||||
let canonical = fs::canonicalize(&base_path)?;
|
let canonical = fs::canonicalize(&base_path)?;
|
||||||
let project_name = canonical
|
let project_name = canonical
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|name| name.to_str())
|
.and_then(|name| name.to_str())
|
||||||
.unwrap_or("new-project");
|
.unwrap_or("new-project");
|
||||||
|
|
||||||
let project_params = ProjectParams {
|
let project_params = ProjectParams {
|
||||||
name: project_name.to_owned(),
|
name: project_name.to_owned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match options.kind {
|
match self.kind {
|
||||||
InitKind::Place => init_place(&base_path, project_params),
|
InitKind::Place => init_place(&base_path, project_params)?,
|
||||||
InitKind::Model => init_model(&base_path, project_params),
|
InitKind::Model => init_model(&base_path, project_params)?,
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Created project successfully.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_place(base_path: &Path, project_params: ProjectParams) -> Result<(), anyhow::Error> {
|
/// The templates we support for initializing a Rojo project.
|
||||||
eprintln!("Creating new place project '{}'", project_params.name);
|
#[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);
|
||||||
|
|
||||||
let project_file = project_params.render_template(PLACE_PROJECT);
|
let project_file = project_params.render_template(PLACE_PROJECT);
|
||||||
try_create_project(base_path, &project_file)?;
|
try_create_project(base_path, &project_file)?;
|
||||||
@@ -88,13 +123,11 @@ fn init_place(base_path: &Path, project_params: ProjectParams) -> Result<(), any
|
|||||||
let git_ignore = project_params.render_template(PLACE_GIT_IGNORE);
|
let git_ignore = project_params.render_template(PLACE_GIT_IGNORE);
|
||||||
try_git_init(base_path, &git_ignore)?;
|
try_git_init(base_path, &git_ignore)?;
|
||||||
|
|
||||||
eprintln!("Created project successfully.");
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_model(base_path: &Path, project_params: ProjectParams) -> Result<(), anyhow::Error> {
|
fn init_model(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> {
|
||||||
eprintln!("Creating new model project '{}'", project_params.name);
|
println!("Creating new model project '{}'", project_params.name);
|
||||||
|
|
||||||
let project_file = project_params.render_template(MODEL_PROJECT);
|
let project_file = project_params.render_template(MODEL_PROJECT);
|
||||||
try_create_project(base_path, &project_file)?;
|
try_create_project(base_path, &project_file)?;
|
||||||
@@ -111,8 +144,6 @@ fn init_model(base_path: &Path, project_params: ProjectParams) -> Result<(), any
|
|||||||
let git_ignore = project_params.render_template(MODEL_GIT_IGNORE);
|
let git_ignore = project_params.render_template(MODEL_GIT_IGNORE);
|
||||||
try_git_init(base_path, &git_ignore)?;
|
try_git_init(base_path, &git_ignore)?;
|
||||||
|
|
||||||
eprintln!("Created project successfully.");
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +169,7 @@ fn try_git_init(path: &Path, git_ignore: &str) -> Result<(), anyhow::Error> {
|
|||||||
let status = Command::new("git").arg("init").current_dir(path).status()?;
|
let status = Command::new("git").arg("init").current_dir(path).status()?;
|
||||||
|
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
return Err(Error::GitInit.into());
|
bail!("git init failed: status code {:?}", status.code());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,13 +226,15 @@ fn try_create_project(base_path: &Path, contents: &str) -> Result<(), anyhow::Er
|
|||||||
let file_res = OpenOptions::new()
|
let file_res = OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.create_new(true)
|
.create_new(true)
|
||||||
.open(project_path);
|
.open(&project_path);
|
||||||
|
|
||||||
let mut file = match file_res {
|
let mut file = match file_res {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return match err.kind() {
|
return match err.kind() {
|
||||||
io::ErrorKind::AlreadyExists => Err(Error::AlreadyExists.into()),
|
io::ErrorKind::AlreadyExists => {
|
||||||
|
bail!("Project file already exists: {}", project_path.display())
|
||||||
|
}
|
||||||
_ => Err(err.into()),
|
_ => Err(err.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
238
src/cli/mod.rs
238
src/cli/mod.rs
@@ -2,29 +2,24 @@
|
|||||||
|
|
||||||
mod build;
|
mod build;
|
||||||
mod doc;
|
mod doc;
|
||||||
|
mod fmt_project;
|
||||||
mod init;
|
mod init;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
mod serve;
|
mod serve;
|
||||||
mod upload;
|
mod upload;
|
||||||
|
|
||||||
use std::{
|
use std::{borrow::Cow, env, path::Path, str::FromStr};
|
||||||
borrow::Cow,
|
|
||||||
env,
|
|
||||||
error::Error,
|
|
||||||
fmt,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
str::FromStr,
|
|
||||||
};
|
|
||||||
|
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub use self::build::*;
|
pub use self::build::BuildCommand;
|
||||||
pub use self::doc::*;
|
pub use self::doc::DocCommand;
|
||||||
pub use self::init::*;
|
pub use self::fmt_project::FmtProjectCommand;
|
||||||
pub use self::plugin::*;
|
pub use self::init::{InitCommand, InitKind};
|
||||||
pub use self::serve::*;
|
pub use self::plugin::{PluginCommand, PluginSubcommand};
|
||||||
pub use self::upload::*;
|
pub use self::serve::ServeCommand;
|
||||||
|
pub use self::upload::UploadCommand;
|
||||||
|
|
||||||
/// Command line options that Rojo accepts, defined using the structopt crate.
|
/// Command line options that Rojo accepts, defined using the structopt crate.
|
||||||
#[derive(Debug, StructOpt)]
|
#[derive(Debug, StructOpt)]
|
||||||
@@ -38,6 +33,20 @@ pub struct Options {
|
|||||||
pub subcommand: Subcommand,
|
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)]
|
#[derive(Debug, StructOpt)]
|
||||||
pub struct GlobalOptions {
|
pub struct GlobalOptions {
|
||||||
/// Sets verbosity level. Can be specified multiple times.
|
/// Sets verbosity level. Can be specified multiple times.
|
||||||
@@ -99,214 +108,19 @@ pub struct ColorChoiceParseError {
|
|||||||
|
|
||||||
#[derive(Debug, StructOpt)]
|
#[derive(Debug, StructOpt)]
|
||||||
pub enum Subcommand {
|
pub enum Subcommand {
|
||||||
/// Creates a new Rojo project.
|
|
||||||
Init(InitCommand),
|
Init(InitCommand),
|
||||||
|
|
||||||
/// Serves the project's files for use with the Rojo Studio plugin.
|
|
||||||
Serve(ServeCommand),
|
Serve(ServeCommand),
|
||||||
|
|
||||||
/// Generates a model or place file from the project.
|
|
||||||
Build(BuildCommand),
|
Build(BuildCommand),
|
||||||
|
|
||||||
/// Generates a place or model file out of the project and uploads it to Roblox.
|
|
||||||
Upload(UploadCommand),
|
Upload(UploadCommand),
|
||||||
|
FmtProject(FmtProjectCommand),
|
||||||
/// Open Rojo's documentation in your browser.
|
Doc(DocCommand),
|
||||||
Doc,
|
|
||||||
|
|
||||||
/// Manages Rojo's Roblox Studio plugin.
|
|
||||||
Plugin(PluginCommand),
|
Plugin(PluginCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes a new Rojo project.
|
pub(super) fn resolve_path(path: &Path) -> Cow<'_, Path> {
|
||||||
#[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 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() {
|
if path.is_absolute() {
|
||||||
Cow::Borrowed(path)
|
Cow::Borrowed(path)
|
||||||
} else {
|
} else {
|
||||||
Cow::Owned(env::current_dir().unwrap().join(path))
|
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,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,26 +3,50 @@ use std::{
|
|||||||
io::BufWriter,
|
io::BufWriter,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use memofs::{InMemoryFs, Vfs, VfsSnapshot};
|
use memofs::{InMemoryFs, Vfs, VfsSnapshot};
|
||||||
use roblox_install::RobloxStudio;
|
use roblox_install::RobloxStudio;
|
||||||
|
use structopt::StructOpt;
|
||||||
|
|
||||||
use crate::{
|
use crate::serve_session::ServeSession;
|
||||||
cli::{PluginCommand, PluginSubcommand},
|
|
||||||
serve_session::ServeSession,
|
|
||||||
};
|
|
||||||
|
|
||||||
static PLUGIN_BINCODE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/plugin.bincode"));
|
static PLUGIN_BINCODE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/plugin.bincode"));
|
||||||
static PLUGIN_FILE_NAME: &str = "RojoManagedPlugin.rbxm";
|
static PLUGIN_FILE_NAME: &str = "RojoManagedPlugin.rbxm";
|
||||||
|
|
||||||
pub fn plugin(options: PluginCommand) -> Result<()> {
|
/// Install Rojo's plugin.
|
||||||
match options.subcommand {
|
#[derive(Debug, StructOpt)]
|
||||||
PluginSubcommand::Install => install_plugin(),
|
pub struct PluginCommand {
|
||||||
PluginSubcommand::Uninstall => uninstall_plugin(),
|
#[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 install_plugin() -> Result<()> {
|
impl PluginSubcommand {
|
||||||
|
pub fn run(self) -> anyhow::Result<()> {
|
||||||
|
match self {
|
||||||
|
PluginSubcommand::Install => install_plugin(),
|
||||||
|
PluginSubcommand::Uninstall => uninstall_plugin(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_plugin() -> anyhow::Result<()> {
|
||||||
let plugin_snapshot: VfsSnapshot = bincode::deserialize(PLUGIN_BINCODE)
|
let plugin_snapshot: VfsSnapshot = bincode::deserialize(PLUGIN_BINCODE)
|
||||||
.expect("Rojo's plugin was not properly packed into Rojo's binary");
|
.expect("Rojo's plugin was not properly packed into Rojo's binary");
|
||||||
|
|
||||||
@@ -49,12 +73,12 @@ pub fn install_plugin() -> Result<()> {
|
|||||||
let tree = session.tree();
|
let tree = session.tree();
|
||||||
let root_id = tree.get_root_id();
|
let root_id = tree.get_root_id();
|
||||||
|
|
||||||
rbx_binary::encode(tree.inner(), &[root_id], &mut file)?;
|
rbx_binary::to_writer(&mut file, tree.inner(), &[root_id])?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uninstall_plugin() -> Result<()> {
|
fn uninstall_plugin() -> anyhow::Result<()> {
|
||||||
let studio = RobloxStudio::locate()?;
|
let studio = RobloxStudio::locate()?;
|
||||||
|
|
||||||
let plugin_path = studio.plugins_path().join(PLUGIN_FILE_NAME);
|
let plugin_path = studio.plugins_path().join(PLUGIN_FILE_NAME);
|
||||||
|
|||||||
@@ -1,51 +1,82 @@
|
|||||||
use std::{
|
use std::{
|
||||||
io::{self, Write},
|
io::{self, Write},
|
||||||
|
net::{IpAddr, Ipv4Addr},
|
||||||
|
path::PathBuf,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use memofs::Vfs;
|
use memofs::Vfs;
|
||||||
|
use structopt::StructOpt;
|
||||||
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
|
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
|
||||||
|
|
||||||
use crate::{
|
use crate::{serve_session::ServeSession, web::LiveServer};
|
||||||
cli::{GlobalOptions, ServeCommand},
|
|
||||||
serve_session::ServeSession,
|
|
||||||
web::LiveServer,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
use super::{resolve_path, GlobalOptions};
|
||||||
|
|
||||||
|
const DEFAULT_BIND_ADDRESS: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
|
||||||
const DEFAULT_PORT: u16 = 34872;
|
const DEFAULT_PORT: u16 = 34872;
|
||||||
|
|
||||||
pub fn serve(global: GlobalOptions, options: ServeCommand) -> Result<()> {
|
/// Expose a Rojo project to the Rojo Studio plugin.
|
||||||
let vfs = Vfs::new_default();
|
#[derive(Debug, StructOpt)]
|
||||||
|
pub struct ServeCommand {
|
||||||
|
/// Path to the project to serve. Defaults to the current directory.
|
||||||
|
#[structopt(default_value = "")]
|
||||||
|
pub project: PathBuf,
|
||||||
|
|
||||||
let session = Arc::new(ServeSession::new(vfs, &options.absolute_project())?);
|
/// The IP address to listen on. Defaults to `127.0.0.1`.
|
||||||
|
#[structopt(long)]
|
||||||
|
pub address: Option<IpAddr>,
|
||||||
|
|
||||||
let port = options
|
/// The port to listen on. Defaults to the project's preference, or `34872` if
|
||||||
.port
|
/// it has none.
|
||||||
.or_else(|| session.project_port())
|
#[structopt(long)]
|
||||||
.unwrap_or(DEFAULT_PORT);
|
pub port: Option<u16>,
|
||||||
|
|
||||||
let server = LiveServer::new(session);
|
|
||||||
|
|
||||||
let _ = show_start_message(port, global.color.into());
|
|
||||||
server.start(port);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_start_message(port: u16, color: ColorChoice) -> io::Result<()> {
|
impl ServeCommand {
|
||||||
|
pub fn run(self, global: GlobalOptions) -> anyhow::Result<()> {
|
||||||
|
let project_path = resolve_path(&self.project);
|
||||||
|
|
||||||
|
let vfs = Vfs::new_default();
|
||||||
|
|
||||||
|
let session = Arc::new(ServeSession::new(vfs, &project_path)?);
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 writer = BufferWriter::stdout(color);
|
||||||
let mut buffer = writer.buffer();
|
let mut buffer = writer.buffer();
|
||||||
|
|
||||||
writeln!(&mut buffer, "Rojo server listening:")?;
|
writeln!(&mut buffer, "Rojo server listening:")?;
|
||||||
|
|
||||||
write!(&mut buffer, " Address: ")?;
|
write!(&mut buffer, " Address: ")?;
|
||||||
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
|
buffer.set_color(&green)?;
|
||||||
writeln!(&mut buffer, "localhost")?;
|
if bind_address.is_loopback() {
|
||||||
|
writeln!(&mut buffer, "localhost")?;
|
||||||
|
} else {
|
||||||
|
writeln!(&mut buffer, "{}", bind_address)?;
|
||||||
|
}
|
||||||
|
|
||||||
buffer.set_color(&ColorSpec::new())?;
|
buffer.set_color(&ColorSpec::new())?;
|
||||||
write!(&mut buffer, " Port: ")?;
|
write!(&mut buffer, " Port: ")?;
|
||||||
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
|
buffer.set_color(&green)?;
|
||||||
writeln!(&mut buffer, "{}", port)?;
|
writeln!(&mut buffer, "{}", port)?;
|
||||||
|
|
||||||
writeln!(&mut buffer)?;
|
writeln!(&mut buffer)?;
|
||||||
@@ -53,7 +84,7 @@ fn show_start_message(port: u16, color: ColorChoice) -> io::Result<()> {
|
|||||||
buffer.set_color(&ColorSpec::new())?;
|
buffer.set_color(&ColorSpec::new())?;
|
||||||
write!(&mut buffer, "Visit ")?;
|
write!(&mut buffer, "Visit ")?;
|
||||||
|
|
||||||
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
|
buffer.set_color(&green)?;
|
||||||
write!(&mut buffer, "http://localhost:{}/", port)?;
|
write!(&mut buffer, "http://localhost:{}/", port)?;
|
||||||
|
|
||||||
buffer.set_color(&ColorSpec::new())?;
|
buffer.set_color(&ColorSpec::new())?;
|
||||||
|
|||||||
@@ -1,69 +1,126 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use anyhow::{bail, format_err, Context};
|
||||||
use memofs::Vfs;
|
use memofs::Vfs;
|
||||||
use reqwest::header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT};
|
use reqwest::{
|
||||||
use thiserror::Error;
|
header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT},
|
||||||
|
StatusCode,
|
||||||
|
};
|
||||||
|
use structopt::StructOpt;
|
||||||
|
|
||||||
use crate::{auth_cookie::get_auth_cookie, cli::UploadCommand, serve_session::ServeSession};
|
use crate::{auth_cookie::get_auth_cookie, serve_session::ServeSession};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
use super::resolve_path;
|
||||||
enum Error {
|
|
||||||
#[error("Rojo could not find your Roblox auth cookie. Please pass one via --cookie.")]
|
|
||||||
NeedAuthCookie,
|
|
||||||
|
|
||||||
#[error("The Roblox API returned an unexpected error: {body}")]
|
/// Builds the project and uploads it to Roblox.
|
||||||
RobloxApi { body: String },
|
#[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,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upload(options: UploadCommand) -> Result<(), anyhow::Error> {
|
impl UploadCommand {
|
||||||
let cookie = options
|
pub fn run(self) -> Result<(), anyhow::Error> {
|
||||||
.cookie
|
let project_path = resolve_path(&self.project);
|
||||||
.clone()
|
|
||||||
.or_else(get_auth_cookie)
|
|
||||||
.ok_or(Error::NeedAuthCookie)?;
|
|
||||||
|
|
||||||
let vfs = Vfs::new_default();
|
let cookie = self.cookie.or_else(get_auth_cookie).context(
|
||||||
|
"Rojo could not find your Roblox auth cookie. Please pass one via --cookie.",
|
||||||
|
)?;
|
||||||
|
|
||||||
let session = ServeSession::new(vfs, &options.absolute_project())?;
|
let vfs = Vfs::new_default();
|
||||||
|
|
||||||
let tree = session.tree();
|
let session = ServeSession::new(vfs, project_path)?;
|
||||||
let inner_tree = tree.inner();
|
|
||||||
let root_id = inner_tree.get_root_id();
|
|
||||||
let root_instance = inner_tree.get_instance(root_id).unwrap();
|
|
||||||
|
|
||||||
let encode_ids = match root_instance.class_name.as_str() {
|
let tree = session.tree();
|
||||||
"DataModel" => root_instance.get_children_ids().to_vec(),
|
let inner_tree = tree.inner();
|
||||||
_ => vec![root_id],
|
let root = inner_tree.root();
|
||||||
};
|
|
||||||
|
|
||||||
let mut buffer = Vec::new();
|
let encode_ids = match root.class.as_str() {
|
||||||
|
"DataModel" => root.children().to_vec(),
|
||||||
|
_ => vec![root.referent()],
|
||||||
|
};
|
||||||
|
|
||||||
log::trace!("Encoding XML model");
|
let mut buffer = Vec::new();
|
||||||
let config = rbx_xml::EncodeOptions::new()
|
|
||||||
.property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown);
|
|
||||||
|
|
||||||
rbx_xml::to_writer(&mut buffer, tree.inner(), &encode_ids, config)?;
|
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
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://data.roblox.com/Data/Upload.ashx?assetid={}",
|
"https://data.roblox.com/Data/Upload.ashx?assetid={}",
|
||||||
options.asset_id
|
asset_id
|
||||||
);
|
);
|
||||||
|
|
||||||
log::trace!("POSTing to {}", url);
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let mut response = client
|
|
||||||
.post(&url)
|
|
||||||
.header(COOKIE, format!(".ROBLOSECURITY={}", &cookie))
|
|
||||||
.header(USER_AGENT, "Roblox/WinInet")
|
|
||||||
.header("Requester", "Client")
|
|
||||||
.header(CONTENT_TYPE, "application/xml")
|
|
||||||
.header(ACCEPT, "application/json")
|
|
||||||
.body(buffer)
|
|
||||||
.send()?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
let build_request = move || {
|
||||||
return Err(Error::RobloxApi {
|
client
|
||||||
body: response.text()?,
|
.post(&url)
|
||||||
|
.header(COOKIE, format!(".ROBLOSECURITY={}", cookie))
|
||||||
|
.header(USER_AGENT, "Roblox/WinInet")
|
||||||
|
.header(CONTENT_TYPE, "application/xml")
|
||||||
|
.header(ACCEPT, "application/json")
|
||||||
|
.body(buffer.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!("Uploading to Roblox...");
|
||||||
|
let mut response = build_request().send()?;
|
||||||
|
|
||||||
|
// Starting in Feburary, 2021, the upload endpoint performs CSRF challenges.
|
||||||
|
// If we receive an HTTP 403 with a X-CSRF-Token reply, we should retry the
|
||||||
|
// request, echoing the value of that header.
|
||||||
|
if response.status() == StatusCode::FORBIDDEN {
|
||||||
|
if let Some(csrf_token) = response.headers().get("X-CSRF-Token") {
|
||||||
|
log::debug!("Received CSRF challenge, retrying with token...");
|
||||||
|
response = build_request().header("X-CSRF-Token", csrf_token).send()?;
|
||||||
}
|
}
|
||||||
.into());
|
}
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
if !status.is_success() {
|
||||||
|
bail!(
|
||||||
|
"The Roblox API returned an unexpected error: {}",
|
||||||
|
response.text()?
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
18
src/error.rs
18
src/error.rs
@@ -1,18 +0,0 @@
|
|||||||
use std::{error::Error, fmt};
|
|
||||||
|
|
||||||
/// Wrapper type to print errors with source-chasing.
|
|
||||||
pub struct ErrorDisplay<E>(pub E);
|
|
||||||
|
|
||||||
impl<E: Error> fmt::Display for ErrorDisplay<E> {
|
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
writeln!(formatter, "{}", self.0)?;
|
|
||||||
|
|
||||||
let mut current_err: &dyn Error = &self.0;
|
|
||||||
while let Some(source) = current_err.source() {
|
|
||||||
writeln!(formatter, " caused by {}", source)?;
|
|
||||||
current_err = &*source;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,13 +9,13 @@ mod tree_view;
|
|||||||
|
|
||||||
mod auth_cookie;
|
mod auth_cookie;
|
||||||
mod change_processor;
|
mod change_processor;
|
||||||
mod error;
|
|
||||||
mod glob;
|
mod glob;
|
||||||
mod lua_ast;
|
mod lua_ast;
|
||||||
mod message_queue;
|
mod message_queue;
|
||||||
mod multimap;
|
mod multimap;
|
||||||
mod path_serializer;
|
mod path_serializer;
|
||||||
mod project;
|
mod project;
|
||||||
|
mod resolution;
|
||||||
mod serve_session;
|
mod serve_session;
|
||||||
mod session_id;
|
mod session_id;
|
||||||
mod snapshot;
|
mod snapshot;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
//! Defines module for defining a small Lua AST for simple codegen.
|
//! Defines module for defining a small Lua AST for simple codegen. Rojo uses
|
||||||
|
//! this module to convert JSON into generated Lua code.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{self, Write},
|
fmt::{self, Write},
|
||||||
|
|||||||
@@ -1,26 +1,6 @@
|
|||||||
use std::sync::{Mutex, RwLock};
|
use std::sync::{Mutex, RwLock};
|
||||||
|
|
||||||
use futures::sync::oneshot;
|
use futures::channel::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.
|
/// A message queue with persistent history that can be subscribed to.
|
||||||
///
|
///
|
||||||
@@ -97,3 +77,23 @@ impl<T: Clone> MessageQueue<T> {
|
|||||||
self.messages.read().unwrap().len() as u32
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use rbx_dom_weak::UnresolvedRbxValue;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::glob::Glob;
|
use crate::{glob::Glob, resolution::UnresolvedValue};
|
||||||
|
|
||||||
static PROJECT_FILENAME: &str = "default.project.json";
|
static PROJECT_FILENAME: &str = "default.project.json";
|
||||||
|
|
||||||
@@ -58,6 +57,16 @@ pub struct Project {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub serve_place_ids: Option<HashSet<u64>>,
|
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
|
/// A list of globs, relative to the folder the project file is in, that
|
||||||
/// match files that should be excluded if Rojo encounters them.
|
/// match files that should be excluded if Rojo encounters them.
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
@@ -183,7 +192,7 @@ pub struct ProjectNode {
|
|||||||
default,
|
default,
|
||||||
skip_serializing_if = "HashMap::is_empty"
|
skip_serializing_if = "HashMap::is_empty"
|
||||||
)]
|
)]
|
||||||
pub properties: HashMap<String, UnresolvedRbxValue>,
|
pub properties: HashMap<String, UnresolvedValue>,
|
||||||
|
|
||||||
/// Defines the behavior when Rojo encounters unknown instances in Roblox
|
/// Defines the behavior when Rojo encounters unknown instances in Roblox
|
||||||
/// Studio during live sync. `$ignoreUnknownInstances` should be considered
|
/// Studio during live sync. `$ignoreUnknownInstances` should be considered
|
||||||
|
|||||||
273
src/resolution.rs
Normal file
273
src/resolution.rs
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
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)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ use std::{
|
|||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use memofs::IoResultExt;
|
use memofs::IoResultExt;
|
||||||
use memofs::Vfs;
|
use memofs::Vfs;
|
||||||
use rbx_dom_weak::RbxInstanceProperties;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -19,13 +18,15 @@ use crate::{
|
|||||||
project::{Project, ProjectError},
|
project::{Project, ProjectError},
|
||||||
session_id::SessionId,
|
session_id::SessionId,
|
||||||
snapshot::{
|
snapshot::{
|
||||||
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext,
|
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext, InstanceSnapshot,
|
||||||
InstancePropertiesWithMeta, PatchSet, RojoTree,
|
PatchSet, RojoTree,
|
||||||
},
|
},
|
||||||
snapshot_middleware::snapshot_from_vfs,
|
snapshot_middleware::snapshot_from_vfs,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Contains all of the state for a Rojo serve session.
|
/// 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.
|
||||||
///
|
///
|
||||||
/// Nothing here is specific to any Rojo interface. Though the primary way to
|
/// 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
|
/// interact with a serve session is Rojo's HTTP right now, there's no reason
|
||||||
@@ -118,14 +119,7 @@ impl ServeSession {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
|
let mut tree = RojoTree::new(InstanceSnapshot::new());
|
||||||
properties: RbxInstanceProperties {
|
|
||||||
name: "ROOT".to_owned(),
|
|
||||||
class_name: "Folder".to_owned(),
|
|
||||||
properties: Default::default(),
|
|
||||||
},
|
|
||||||
metadata: Default::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let root_id = tree.get_root_id();
|
let root_id = tree.get_root_id();
|
||||||
|
|
||||||
@@ -203,6 +197,14 @@ impl ServeSession {
|
|||||||
self.root_project.serve_port
|
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 {
|
pub fn start_time(&self) -> Instant {
|
||||||
self.start_time
|
self.start_time
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
use std::{borrow::Cow, collections::HashMap};
|
use std::{borrow::Cow, collections::HashMap};
|
||||||
|
|
||||||
use rbx_dom_weak::{RbxId, RbxTree, RbxValue};
|
use rbx_dom_weak::{
|
||||||
|
types::{Ref, Variant},
|
||||||
|
WeakDom,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::InstanceMetadata;
|
use super::InstanceMetadata;
|
||||||
@@ -11,11 +14,12 @@ use super::InstanceMetadata;
|
|||||||
///
|
///
|
||||||
// Possible future improvements:
|
// Possible future improvements:
|
||||||
// - Use refcounted/interned strings
|
// - Use refcounted/interned strings
|
||||||
// - Replace use of RbxValue with a sum of RbxValue + borrowed value
|
// - Replace use of Variant with a sum of Variant + borrowed value
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct InstanceSnapshot {
|
pub struct InstanceSnapshot {
|
||||||
|
// FIXME: Don't use Option<Ref> anymore!
|
||||||
/// A temporary ID applied to the snapshot that's used for Ref properties.
|
/// A temporary ID applied to the snapshot that's used for Ref properties.
|
||||||
pub snapshot_id: Option<RbxId>,
|
pub snapshot_id: Option<Ref>,
|
||||||
|
|
||||||
/// Rojo-specific metadata associated with the instance.
|
/// Rojo-specific metadata associated with the instance.
|
||||||
pub metadata: InstanceMetadata,
|
pub metadata: InstanceMetadata,
|
||||||
@@ -27,7 +31,7 @@ pub struct InstanceSnapshot {
|
|||||||
pub class_name: Cow<'static, str>,
|
pub class_name: Cow<'static, str>,
|
||||||
|
|
||||||
/// All other properties of the instance, weakly-typed.
|
/// All other properties of the instance, weakly-typed.
|
||||||
pub properties: HashMap<String, RbxValue>,
|
pub properties: HashMap<String, Variant>,
|
||||||
|
|
||||||
/// The children of the instance represented as more snapshots.
|
/// The children of the instance represented as more snapshots.
|
||||||
///
|
///
|
||||||
@@ -61,7 +65,16 @@ impl InstanceSnapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn properties(self, properties: impl Into<HashMap<String, RbxValue>>) -> Self {
|
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 {
|
||||||
Self {
|
Self {
|
||||||
properties: properties.into(),
|
properties: properties.into(),
|
||||||
..self
|
..self
|
||||||
@@ -75,6 +88,13 @@ 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 {
|
pub fn metadata(self, metadata: impl Into<InstanceMetadata>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
metadata: metadata.into(),
|
metadata: metadata.into(),
|
||||||
@@ -82,15 +102,13 @@ impl InstanceSnapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_tree(tree: &RbxTree, id: RbxId) -> Self {
|
pub fn from_tree(tree: &WeakDom, id: Ref) -> Self {
|
||||||
let instance = tree
|
let instance = tree.get_by_ref(id).expect("instance did not exist in tree");
|
||||||
.get_instance(id)
|
|
||||||
.expect("instance did not exist in tree");
|
|
||||||
|
|
||||||
let children = instance
|
let children = instance
|
||||||
.get_children_ids()
|
.children()
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.copied()
|
||||||
.map(|id| Self::from_tree(tree, id))
|
.map(|id| Self::from_tree(tree, id))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -98,7 +116,7 @@ impl InstanceSnapshot {
|
|||||||
snapshot_id: Some(id),
|
snapshot_id: Some(id),
|
||||||
metadata: InstanceMetadata::default(),
|
metadata: InstanceMetadata::default(),
|
||||||
name: Cow::Owned(instance.name.clone()),
|
name: Cow::Owned(instance.name.clone()),
|
||||||
class_name: Cow::Owned(instance.class_name.clone()),
|
class_name: Cow::Owned(instance.class.clone()),
|
||||||
properties: instance.properties.clone(),
|
properties: instance.properties.clone(),
|
||||||
children,
|
children,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,19 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use rbx_dom_weak::{RbxId, RbxValue};
|
use rbx_dom_weak::types::{Ref, Variant};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::{InstanceMetadata, InstanceSnapshot};
|
use super::{InstanceMetadata, InstanceSnapshot};
|
||||||
|
|
||||||
/// A set of different kinds of patches that can be applied to an RbxTree.
|
/// A set of different kinds of patches that can be applied to an WeakDom.
|
||||||
///
|
///
|
||||||
/// These patches shouldn't be persisted: there's no mechanism in place to make
|
/// 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
|
/// sure that another patch wasn't applied before this one that could cause a
|
||||||
/// conflict!
|
/// conflict!
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct PatchSet {
|
pub struct PatchSet {
|
||||||
pub removed_instances: Vec<RbxId>,
|
pub removed_instances: Vec<Ref>,
|
||||||
pub added_instances: Vec<PatchAdd>,
|
pub added_instances: Vec<PatchAdd>,
|
||||||
pub updated_instances: Vec<PatchUpdate>,
|
pub updated_instances: Vec<PatchUpdate>,
|
||||||
}
|
}
|
||||||
@@ -32,20 +32,20 @@ impl<'a> PatchSet {
|
|||||||
/// A patch containing an instance that was added to the tree.
|
/// A patch containing an instance that was added to the tree.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct PatchAdd {
|
pub struct PatchAdd {
|
||||||
pub parent_id: RbxId,
|
pub parent_id: Ref,
|
||||||
pub instance: InstanceSnapshot,
|
pub instance: InstanceSnapshot,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A patch indicating that properties of an instance changed.
|
/// A patch indicating that properties of an instance changed.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct PatchUpdate {
|
pub struct PatchUpdate {
|
||||||
pub id: RbxId,
|
pub id: Ref,
|
||||||
pub changed_name: Option<String>,
|
pub changed_name: Option<String>,
|
||||||
pub changed_class_name: Option<String>,
|
pub changed_class_name: Option<String>,
|
||||||
|
|
||||||
/// Contains all changed properties. If a property is assigned to `None`,
|
/// Contains all changed properties. If a property is assigned to `None`,
|
||||||
/// then that property has been removed.
|
/// then that property has been removed.
|
||||||
pub changed_properties: HashMap<String, Option<RbxValue>>,
|
pub changed_properties: HashMap<String, Option<Variant>>,
|
||||||
|
|
||||||
/// Changed Rojo-specific metadata, if any of it changed.
|
/// Changed Rojo-specific metadata, if any of it changed.
|
||||||
pub changed_metadata: Option<InstanceMetadata>,
|
pub changed_metadata: Option<InstanceMetadata>,
|
||||||
@@ -63,8 +63,8 @@ pub struct PatchUpdate {
|
|||||||
// current values in all fields.
|
// current values in all fields.
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct AppliedPatchSet {
|
pub struct AppliedPatchSet {
|
||||||
pub removed: Vec<RbxId>,
|
pub removed: Vec<Ref>,
|
||||||
pub added: Vec<RbxId>,
|
pub added: Vec<Ref>,
|
||||||
pub updated: Vec<AppliedPatchUpdate>,
|
pub updated: Vec<AppliedPatchUpdate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,17 +80,17 @@ impl AppliedPatchSet {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AppliedPatchUpdate {
|
pub struct AppliedPatchUpdate {
|
||||||
pub id: RbxId,
|
pub id: Ref,
|
||||||
|
|
||||||
// TODO: Store previous values in order to detect application conflicts
|
// TODO: Store previous values in order to detect application conflicts
|
||||||
pub changed_name: Option<String>,
|
pub changed_name: Option<String>,
|
||||||
pub changed_class_name: Option<String>,
|
pub changed_class_name: Option<String>,
|
||||||
pub changed_properties: HashMap<String, Option<RbxValue>>,
|
pub changed_properties: HashMap<String, Option<Variant>>,
|
||||||
pub changed_metadata: Option<InstanceMetadata>,
|
pub changed_metadata: Option<InstanceMetadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppliedPatchUpdate {
|
impl AppliedPatchUpdate {
|
||||||
pub fn new(id: RbxId) -> Self {
|
pub fn new(id: Ref) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
changed_name: None,
|
changed_name: None,
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use rbx_dom_weak::{RbxId, RbxInstanceProperties, RbxValue};
|
use rbx_dom_weak::types::{Ref, Variant};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
patch::{AppliedPatchSet, AppliedPatchUpdate, PatchSet, PatchUpdate},
|
patch::{AppliedPatchSet, AppliedPatchUpdate, PatchSet, PatchUpdate},
|
||||||
InstancePropertiesWithMeta, InstanceSnapshot, RojoTree,
|
InstanceSnapshot, RojoTree,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Consumes the input `PatchSet`, applying all of its prescribed changes to the
|
/// 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 {
|
struct PatchApplyContext {
|
||||||
/// A map from transient snapshot IDs (generated by snapshot middleware) to
|
/// 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
|
/// instance IDs in the actual tree. These are both the same data type so
|
||||||
/// that they fit into the same `RbxValue::Ref` type.
|
/// that they fit into the same `Variant::Ref` type.
|
||||||
///
|
///
|
||||||
/// At this point in the patch process, IDs in instance properties have been
|
/// At this point in the patch process, IDs in instance properties have been
|
||||||
/// partially translated from 'snapshot space' into 'tree space' by the
|
/// 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
|
/// #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
|
/// from #1 right now. It could happen if two model files try to reference
|
||||||
/// eachother.
|
/// eachother.
|
||||||
snapshot_id_to_instance_id: HashMap<RbxId, RbxId>,
|
snapshot_id_to_instance_id: HashMap<Ref, Ref>,
|
||||||
|
|
||||||
/// The properties of instances added by the current `PatchSet`.
|
/// 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
|
/// This doesn't affect updated instances, since they're always applied
|
||||||
/// after we've added all the instances from the patch.
|
/// after we've added all the instances from the patch.
|
||||||
added_instance_properties: HashMap<RbxId, HashMap<String, RbxValue>>,
|
added_instance_properties: HashMap<Ref, HashMap<String, Variant>>,
|
||||||
|
|
||||||
/// The current applied patch result, describing changes made to the tree.
|
/// The current applied patch result, describing changes made to the tree.
|
||||||
applied_patch_set: AppliedPatchSet,
|
applied_patch_set: AppliedPatchSet,
|
||||||
@@ -93,11 +93,10 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -
|
|||||||
.expect("Invalid instance ID in deferred property map");
|
.expect("Invalid instance ID in deferred property map");
|
||||||
|
|
||||||
for (key, mut property_value) in properties {
|
for (key, mut property_value) in properties {
|
||||||
if let RbxValue::Ref { value: Some(id) } = property_value {
|
if let Variant::Ref(referent) = property_value {
|
||||||
if let Some(&instance_id) = context.snapshot_id_to_instance_id.get(&id) {
|
if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(&referent)
|
||||||
property_value = RbxValue::Ref {
|
{
|
||||||
value: Some(instance_id),
|
property_value = Variant::Ref(instance_referent);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,50 +107,40 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -
|
|||||||
context.applied_patch_set
|
context.applied_patch_set
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_remove_instance(context: &mut PatchApplyContext, tree: &mut RojoTree, removed_id: RbxId) {
|
fn apply_remove_instance(context: &mut PatchApplyContext, tree: &mut RojoTree, removed_id: Ref) {
|
||||||
match tree.remove_instance(removed_id) {
|
tree.remove(removed_id);
|
||||||
Some(_) => context.applied_patch_set.removed.push(removed_id),
|
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(
|
fn apply_add_child(
|
||||||
context: &mut PatchApplyContext,
|
context: &mut PatchApplyContext,
|
||||||
tree: &mut RojoTree,
|
tree: &mut RojoTree,
|
||||||
parent_id: RbxId,
|
parent_id: Ref,
|
||||||
snapshot: InstanceSnapshot,
|
snapshot: InstanceSnapshot,
|
||||||
) {
|
) {
|
||||||
let properties = InstancePropertiesWithMeta {
|
let snapshot_id = snapshot.snapshot_id;
|
||||||
properties: RbxInstanceProperties {
|
let properties = snapshot.properties;
|
||||||
name: snapshot.name.into_owned(),
|
let children = snapshot.children;
|
||||||
class_name: snapshot.class_name.into_owned(),
|
|
||||||
|
|
||||||
// Property assignment is deferred until after we know about all
|
// Property application is deferred until after all children
|
||||||
// instances in this patch. See `PatchApplyContext` for details.
|
// are constructed. This helps apply referents correctly.
|
||||||
properties: HashMap::new(),
|
let remaining_snapshot = InstanceSnapshot::new()
|
||||||
},
|
.name(snapshot.name)
|
||||||
metadata: snapshot.metadata,
|
.class_name(snapshot.class_name)
|
||||||
};
|
.metadata(snapshot.metadata)
|
||||||
|
.snapshot_id(snapshot.snapshot_id);
|
||||||
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.applied_patch_set.added.push(id);
|
||||||
|
|
||||||
context
|
context.added_instance_properties.insert(id, properties);
|
||||||
.added_instance_properties
|
|
||||||
.insert(id, snapshot.properties);
|
|
||||||
|
|
||||||
if let Some(snapshot_id) = snapshot.snapshot_id {
|
if let Some(snapshot_id) = snapshot_id {
|
||||||
context.snapshot_id_to_instance_id.insert(snapshot_id, id);
|
context.snapshot_id_to_instance_id.insert(snapshot_id, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
for child_snapshot in snapshot.children {
|
for child in children {
|
||||||
apply_add_child(context, tree, id, child_snapshot);
|
apply_add_child(context, tree, id, child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +156,7 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
|
|||||||
Some(instance) => instance,
|
Some(instance) => instance,
|
||||||
None => {
|
None => {
|
||||||
log::warn!(
|
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
|
patch.id
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -189,23 +178,24 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
|
|||||||
// Ref values need to be potentially rewritten from snapshot IDs to
|
// Ref values need to be potentially rewritten from snapshot IDs to
|
||||||
// instance IDs if they referred to an instance that was created as
|
// instance IDs if they referred to an instance that was created as
|
||||||
// part of this patch.
|
// part of this patch.
|
||||||
Some(RbxValue::Ref { value: Some(id) }) => {
|
Some(Variant::Ref(referent)) => {
|
||||||
|
if referent.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// If our ID is not found in this map, then it either refers to
|
// 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
|
// an existing instance NOT added by this patch, or there was an
|
||||||
// error. See `PatchApplyContext::snapshot_id_to_instance_id`
|
// error. See `PatchApplyContext::snapshot_id_to_instance_id`
|
||||||
// for more info.
|
// for more info.
|
||||||
let new_id = context
|
let new_referent = context
|
||||||
.snapshot_id_to_instance_id
|
.snapshot_id_to_instance_id
|
||||||
.get(&id)
|
.get(&referent)
|
||||||
.copied()
|
.copied()
|
||||||
.unwrap_or(id);
|
.unwrap_or(referent);
|
||||||
|
|
||||||
instance.properties_mut().insert(
|
instance
|
||||||
key.clone(),
|
.properties_mut()
|
||||||
RbxValue::Ref {
|
.insert(key.clone(), Variant::Ref(new_referent));
|
||||||
value: Some(new_id),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Some(ref value) => {
|
Some(ref value) => {
|
||||||
instance.properties_mut().insert(key.clone(), value.clone());
|
instance.properties_mut().insert(key.clone(), value.clone());
|
||||||
@@ -225,10 +215,10 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
|
|||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use std::{borrow::Cow, collections::HashMap};
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use maplit::hashmap;
|
use maplit::hashmap;
|
||||||
use rbx_dom_weak::RbxValue;
|
use rbx_dom_weak::types::Variant;
|
||||||
|
|
||||||
use super::super::PatchAdd;
|
use super::super::PatchAdd;
|
||||||
|
|
||||||
@@ -236,14 +226,7 @@ mod test {
|
|||||||
fn add_from_empty() {
|
fn add_from_empty() {
|
||||||
let _ = env_logger::try_init();
|
let _ = env_logger::try_init();
|
||||||
|
|
||||||
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
|
let mut tree = RojoTree::new(InstanceSnapshot::new());
|
||||||
properties: RbxInstanceProperties {
|
|
||||||
name: "Folder".to_owned(),
|
|
||||||
class_name: "Folder".to_owned(),
|
|
||||||
properties: HashMap::new(),
|
|
||||||
},
|
|
||||||
metadata: Default::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let root_id = tree.get_root_id();
|
let root_id = tree.get_root_id();
|
||||||
|
|
||||||
@@ -253,7 +236,7 @@ mod test {
|
|||||||
name: Cow::Borrowed("Foo"),
|
name: Cow::Borrowed("Foo"),
|
||||||
class_name: Cow::Borrowed("Bar"),
|
class_name: Cow::Borrowed("Bar"),
|
||||||
properties: hashmap! {
|
properties: hashmap! {
|
||||||
"Baz".to_owned() => RbxValue::Int32 { value: 5 },
|
"Baz".to_owned() => Variant::Int32(5),
|
||||||
},
|
},
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
};
|
};
|
||||||
@@ -282,18 +265,14 @@ mod test {
|
|||||||
fn update_existing() {
|
fn update_existing() {
|
||||||
let _ = env_logger::try_init();
|
let _ = env_logger::try_init();
|
||||||
|
|
||||||
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
|
let mut tree = RojoTree::new(
|
||||||
properties: RbxInstanceProperties {
|
InstanceSnapshot::new()
|
||||||
name: "OldName".to_owned(),
|
.class_name("OldClassName")
|
||||||
class_name: "OldClassName".to_owned(),
|
.name("OldName")
|
||||||
properties: hashmap! {
|
.property("Foo", 7i32)
|
||||||
"Foo".to_owned() => RbxValue::Int32 { value: 7 },
|
.property("Bar", 3i32)
|
||||||
"Bar".to_owned() => RbxValue::Int32 { value: 3 },
|
.property("Unchanged", -5i32),
|
||||||
"Unchanged".to_owned() => RbxValue::Int32 { value: -5 },
|
);
|
||||||
},
|
|
||||||
},
|
|
||||||
metadata: Default::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let root_id = tree.get_root_id();
|
let root_id = tree.get_root_id();
|
||||||
|
|
||||||
@@ -303,13 +282,13 @@ mod test {
|
|||||||
changed_class_name: Some("NewClassName".to_owned()),
|
changed_class_name: Some("NewClassName".to_owned()),
|
||||||
changed_properties: hashmap! {
|
changed_properties: hashmap! {
|
||||||
// The value of Foo has changed
|
// The value of Foo has changed
|
||||||
"Foo".to_owned() => Some(RbxValue::Int32 { value: 8 }),
|
"Foo".to_owned() => Some(Variant::Int32(8)),
|
||||||
|
|
||||||
// Bar has been deleted
|
// Bar has been deleted
|
||||||
"Bar".to_owned() => None,
|
"Bar".to_owned() => None,
|
||||||
|
|
||||||
// Baz has been added
|
// Baz has been added
|
||||||
"Baz".to_owned() => Some(RbxValue::Int32 { value: 10 }),
|
"Baz".to_owned() => Some(Variant::Int32(10)),
|
||||||
},
|
},
|
||||||
changed_metadata: None,
|
changed_metadata: None,
|
||||||
};
|
};
|
||||||
@@ -322,9 +301,9 @@ mod test {
|
|||||||
apply_patch_set(&mut tree, patch_set);
|
apply_patch_set(&mut tree, patch_set);
|
||||||
|
|
||||||
let expected_properties = hashmap! {
|
let expected_properties = hashmap! {
|
||||||
"Foo".to_owned() => RbxValue::Int32 { value: 8 },
|
"Foo".to_owned() => Variant::Int32(8),
|
||||||
"Baz".to_owned() => RbxValue::Int32 { value: 10 },
|
"Baz".to_owned() => Variant::Int32(10),
|
||||||
"Unchanged".to_owned() => RbxValue::Int32 { value: -5 },
|
"Unchanged".to_owned() => Variant::Int32(-5),
|
||||||
};
|
};
|
||||||
|
|
||||||
let root_instance = tree.get_instance(root_id).unwrap();
|
let root_instance = tree.get_instance(root_id).unwrap();
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use rbx_dom_weak::{RbxId, RbxValue};
|
use rbx_dom_weak::types::{Ref, Variant};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
patch::{PatchAdd, PatchSet, PatchUpdate},
|
patch::{PatchAdd, PatchSet, PatchUpdate},
|
||||||
InstanceSnapshot, InstanceWithMeta, RojoTree,
|
InstanceSnapshot, InstanceWithMeta, RojoTree,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: RbxId) -> PatchSet {
|
pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: Ref) -> PatchSet {
|
||||||
let mut patch_set = PatchSet::new();
|
let mut patch_set = PatchSet::new();
|
||||||
let mut context = ComputePatchContext::default();
|
let mut context = ComputePatchContext::default();
|
||||||
|
|
||||||
@@ -26,17 +26,15 @@ pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: RbxId
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct ComputePatchContext {
|
struct ComputePatchContext {
|
||||||
snapshot_id_to_instance_id: HashMap<RbxId, RbxId>,
|
snapshot_id_to_instance_id: HashMap<Ref, Ref>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rewrite_refs_in_updates(context: &ComputePatchContext, updates: &mut [PatchUpdate]) {
|
fn rewrite_refs_in_updates(context: &ComputePatchContext, updates: &mut [PatchUpdate]) {
|
||||||
for update in updates {
|
for update in updates {
|
||||||
for property_value in update.changed_properties.values_mut() {
|
for property_value in update.changed_properties.values_mut() {
|
||||||
if let Some(RbxValue::Ref { value: Some(id) }) = property_value {
|
if let Some(Variant::Ref(referent)) = property_value {
|
||||||
if let Some(&instance_id) = context.snapshot_id_to_instance_id.get(id) {
|
if let Some(&instance_ref) = context.snapshot_id_to_instance_id.get(referent) {
|
||||||
*property_value = Some(RbxValue::Ref {
|
*property_value = Some(Variant::Ref(instance_ref));
|
||||||
value: Some(instance_id),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,11 +49,9 @@ fn rewrite_refs_in_additions(context: &ComputePatchContext, additions: &mut [Pat
|
|||||||
|
|
||||||
fn rewrite_refs_in_snapshot(context: &ComputePatchContext, snapshot: &mut InstanceSnapshot) {
|
fn rewrite_refs_in_snapshot(context: &ComputePatchContext, snapshot: &mut InstanceSnapshot) {
|
||||||
for property_value in snapshot.properties.values_mut() {
|
for property_value in snapshot.properties.values_mut() {
|
||||||
if let RbxValue::Ref { value: Some(id) } = property_value {
|
if let Variant::Ref(referent) = property_value {
|
||||||
if let Some(&instance_id) = context.snapshot_id_to_instance_id.get(id) {
|
if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(referent) {
|
||||||
*property_value = RbxValue::Ref {
|
*property_value = Variant::Ref(instance_referent);
|
||||||
value: Some(instance_id),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +65,7 @@ fn compute_patch_set_internal(
|
|||||||
context: &mut ComputePatchContext,
|
context: &mut ComputePatchContext,
|
||||||
snapshot: &InstanceSnapshot,
|
snapshot: &InstanceSnapshot,
|
||||||
tree: &RojoTree,
|
tree: &RojoTree,
|
||||||
id: RbxId,
|
id: Ref,
|
||||||
patch_set: &mut PatchSet,
|
patch_set: &mut PatchSet,
|
||||||
) {
|
) {
|
||||||
if let Some(snapshot_id) = snapshot.snapshot_id {
|
if let Some(snapshot_id) = snapshot.snapshot_id {
|
||||||
@@ -154,7 +150,7 @@ fn compute_children_patches(
|
|||||||
context: &mut ComputePatchContext,
|
context: &mut ComputePatchContext,
|
||||||
snapshot: &InstanceSnapshot,
|
snapshot: &InstanceSnapshot,
|
||||||
tree: &RojoTree,
|
tree: &RojoTree,
|
||||||
id: RbxId,
|
id: Ref,
|
||||||
patch_set: &mut PatchSet,
|
patch_set: &mut PatchSet,
|
||||||
) {
|
) {
|
||||||
let instance = tree
|
let instance = tree
|
||||||
@@ -224,9 +220,6 @@ mod test {
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use maplit::hashmap;
|
use maplit::hashmap;
|
||||||
use rbx_dom_weak::RbxInstanceProperties;
|
|
||||||
|
|
||||||
use super::super::InstancePropertiesWithMeta;
|
|
||||||
|
|
||||||
/// This test makes sure that rewriting refs in instance update patches to
|
/// This test makes sure that rewriting refs in instance update patches to
|
||||||
/// instances that already exists works. We should be able to correlate the
|
/// instances that already exists works. We should be able to correlate the
|
||||||
@@ -234,26 +227,17 @@ mod test {
|
|||||||
/// value before returning from compute_patch_set.
|
/// value before returning from compute_patch_set.
|
||||||
#[test]
|
#[test]
|
||||||
fn rewrite_ref_existing_instance_update() {
|
fn rewrite_ref_existing_instance_update() {
|
||||||
let tree = RojoTree::new(InstancePropertiesWithMeta {
|
let tree = RojoTree::new(InstanceSnapshot::new().name("foo").class_name("foo"));
|
||||||
properties: RbxInstanceProperties {
|
|
||||||
name: "foo".to_owned(),
|
|
||||||
class_name: "foo".to_owned(),
|
|
||||||
properties: HashMap::new(),
|
|
||||||
},
|
|
||||||
metadata: Default::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let root_id = tree.get_root_id();
|
let root_id = tree.get_root_id();
|
||||||
|
|
||||||
// This snapshot should be identical to the existing tree except for the
|
// This snapshot should be identical to the existing tree except for the
|
||||||
// addition of a prop named Self, which is a self-referential Ref.
|
// addition of a prop named Self, which is a self-referential Ref.
|
||||||
let snapshot_id = RbxId::new();
|
let snapshot_id = Ref::new();
|
||||||
let snapshot = InstanceSnapshot {
|
let snapshot = InstanceSnapshot {
|
||||||
snapshot_id: Some(snapshot_id),
|
snapshot_id: Some(snapshot_id),
|
||||||
properties: hashmap! {
|
properties: hashmap! {
|
||||||
"Self".to_owned() => RbxValue::Ref {
|
"Self".to_owned() => Variant::Ref(snapshot_id),
|
||||||
value: Some(snapshot_id),
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
metadata: Default::default(),
|
metadata: Default::default(),
|
||||||
@@ -270,9 +254,7 @@ mod test {
|
|||||||
changed_name: None,
|
changed_name: None,
|
||||||
changed_class_name: None,
|
changed_class_name: None,
|
||||||
changed_properties: hashmap! {
|
changed_properties: hashmap! {
|
||||||
"Self".to_owned() => Some(RbxValue::Ref {
|
"Self".to_owned() => Some(Variant::Ref(root_id)),
|
||||||
value: Some(root_id),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
changed_metadata: None,
|
changed_metadata: None,
|
||||||
}],
|
}],
|
||||||
@@ -288,26 +270,17 @@ mod test {
|
|||||||
/// one.
|
/// one.
|
||||||
#[test]
|
#[test]
|
||||||
fn rewrite_ref_existing_instance_addition() {
|
fn rewrite_ref_existing_instance_addition() {
|
||||||
let tree = RojoTree::new(InstancePropertiesWithMeta {
|
let tree = RojoTree::new(InstanceSnapshot::new().name("foo").class_name("foo"));
|
||||||
properties: RbxInstanceProperties {
|
|
||||||
name: "foo".to_owned(),
|
|
||||||
class_name: "foo".to_owned(),
|
|
||||||
properties: HashMap::new(),
|
|
||||||
},
|
|
||||||
metadata: Default::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let root_id = tree.get_root_id();
|
let root_id = tree.get_root_id();
|
||||||
|
|
||||||
// This patch describes the existing instance with a new child added.
|
// This patch describes the existing instance with a new child added.
|
||||||
let snapshot_id = RbxId::new();
|
let snapshot_id = Ref::new();
|
||||||
let snapshot = InstanceSnapshot {
|
let snapshot = InstanceSnapshot {
|
||||||
snapshot_id: Some(snapshot_id),
|
snapshot_id: Some(snapshot_id),
|
||||||
children: vec![InstanceSnapshot {
|
children: vec![InstanceSnapshot {
|
||||||
properties: hashmap! {
|
properties: hashmap! {
|
||||||
"Self".to_owned() => RbxValue::Ref {
|
"Self".to_owned() => Variant::Ref(snapshot_id),
|
||||||
value: Some(snapshot_id),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
snapshot_id: None,
|
snapshot_id: None,
|
||||||
@@ -332,9 +305,7 @@ mod test {
|
|||||||
snapshot_id: None,
|
snapshot_id: None,
|
||||||
metadata: Default::default(),
|
metadata: Default::default(),
|
||||||
properties: hashmap! {
|
properties: hashmap! {
|
||||||
"Self".to_owned() => RbxValue::Ref {
|
"Self".to_owned() => Variant::Ref(root_id),
|
||||||
value: Some(root_id),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
name: Cow::Borrowed("child"),
|
name: Cow::Borrowed("child"),
|
||||||
class_name: Cow::Borrowed("child"),
|
class_name: Cow::Borrowed("child"),
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use insta::assert_yaml_snapshot;
|
use insta::assert_yaml_snapshot;
|
||||||
use maplit::hashmap;
|
use maplit::hashmap;
|
||||||
use rbx_dom_weak::{RbxInstanceProperties, RbxValue};
|
|
||||||
|
|
||||||
use rojo_insta_ext::RedactionMap;
|
use rojo_insta_ext::RedactionMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
snapshot::{apply_patch_set, InstancePropertiesWithMeta, PatchSet, PatchUpdate, RojoTree},
|
snapshot::{apply_patch_set, InstanceSnapshot, PatchSet, PatchUpdate, RojoTree},
|
||||||
tree_view::{intern_tree, view_tree},
|
tree_view::{intern_tree, view_tree},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,9 +48,7 @@ fn add_property() {
|
|||||||
changed_name: None,
|
changed_name: None,
|
||||||
changed_class_name: None,
|
changed_class_name: None,
|
||||||
changed_properties: hashmap! {
|
changed_properties: hashmap! {
|
||||||
"Foo".to_owned() => Some(RbxValue::String {
|
"Foo".to_owned() => Some("Value of Foo".into()),
|
||||||
value: "Value of Foo".to_owned(),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
changed_metadata: None,
|
changed_metadata: None,
|
||||||
}],
|
}],
|
||||||
@@ -78,12 +75,9 @@ fn remove_property() {
|
|||||||
let root_id = tree.get_root_id();
|
let root_id = tree.get_root_id();
|
||||||
let mut root_instance = tree.get_instance_mut(root_id).unwrap();
|
let mut root_instance = tree.get_instance_mut(root_id).unwrap();
|
||||||
|
|
||||||
root_instance.properties_mut().insert(
|
root_instance
|
||||||
"Foo".to_owned(),
|
.properties_mut()
|
||||||
RbxValue::String {
|
.insert("Foo".to_owned(), "Should be removed".into());
|
||||||
value: "Should be removed".to_owned(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let tree_view = view_tree(&tree, &mut redactions);
|
let tree_view = view_tree(&tree, &mut redactions);
|
||||||
@@ -112,12 +106,5 @@ fn remove_property() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn empty_tree() -> RojoTree {
|
fn empty_tree() -> RojoTree {
|
||||||
RojoTree::new(InstancePropertiesWithMeta {
|
RojoTree::new(InstanceSnapshot::new().name("ROOT").class_name("ROOT"))
|
||||||
properties: RbxInstanceProperties {
|
|
||||||
name: "ROOT".to_owned(),
|
|
||||||
class_name: "ROOT".to_owned(),
|
|
||||||
properties: Default::default(),
|
|
||||||
},
|
|
||||||
metadata: Default::default(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ use std::borrow::Cow;
|
|||||||
|
|
||||||
use insta::assert_yaml_snapshot;
|
use insta::assert_yaml_snapshot;
|
||||||
use maplit::hashmap;
|
use maplit::hashmap;
|
||||||
use rbx_dom_weak::{RbxInstanceProperties, RbxValue};
|
|
||||||
|
|
||||||
use rojo_insta_ext::RedactionMap;
|
use rojo_insta_ext::RedactionMap;
|
||||||
|
|
||||||
use crate::snapshot::{compute_patch_set, InstancePropertiesWithMeta, InstanceSnapshot, RojoTree};
|
use crate::snapshot::{compute_patch_set, InstanceSnapshot, RojoTree};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_name_and_class_name() {
|
fn set_name_and_class_name() {
|
||||||
@@ -43,9 +42,7 @@ fn set_property() {
|
|||||||
name: Cow::Borrowed("ROOT"),
|
name: Cow::Borrowed("ROOT"),
|
||||||
class_name: Cow::Borrowed("ROOT"),
|
class_name: Cow::Borrowed("ROOT"),
|
||||||
properties: hashmap! {
|
properties: hashmap! {
|
||||||
"PropertyName".to_owned() => RbxValue::String {
|
"PropertyName".to_owned() => "Hello, world!".into(),
|
||||||
value: "Hello, world!".to_owned(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
};
|
};
|
||||||
@@ -68,9 +65,7 @@ fn remove_property() {
|
|||||||
let mut root_instance = tree.get_instance_mut(root_id).unwrap();
|
let mut root_instance = tree.get_instance_mut(root_id).unwrap();
|
||||||
root_instance.properties_mut().insert(
|
root_instance.properties_mut().insert(
|
||||||
"Foo".to_owned(),
|
"Foo".to_owned(),
|
||||||
RbxValue::String {
|
"This should be removed by the patch.".into(),
|
||||||
value: "This should be removed by the patch.".to_owned(),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,15 +123,8 @@ fn remove_child() {
|
|||||||
{
|
{
|
||||||
let root_id = tree.get_root_id();
|
let root_id = tree.get_root_id();
|
||||||
let new_id = tree.insert_instance(
|
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,
|
root_id,
|
||||||
|
InstanceSnapshot::new().name("Should not appear in snapshot"),
|
||||||
);
|
);
|
||||||
|
|
||||||
redactions.intern(new_id);
|
redactions.intern(new_id);
|
||||||
@@ -158,12 +146,5 @@ fn remove_child() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn empty_tree() -> RojoTree {
|
fn empty_tree() -> RojoTree {
|
||||||
RojoTree::new(InstancePropertiesWithMeta {
|
RojoTree::new(InstanceSnapshot::new().name("ROOT").class_name("ROOT"))
|
||||||
properties: RbxInstanceProperties {
|
|
||||||
name: "ROOT".to_owned(),
|
|
||||||
class_name: "ROOT".to_owned(),
|
|
||||||
properties: Default::default(),
|
|
||||||
},
|
|
||||||
metadata: Default::default(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
source: src/snapshot/tests/apply.rs
|
source: src/snapshot/tests/apply.rs
|
||||||
expression: applied_patch_value
|
expression: applied_patch_value
|
||||||
|
|
||||||
---
|
---
|
||||||
removed: []
|
removed: []
|
||||||
added: []
|
added: []
|
||||||
@@ -10,6 +11,6 @@ updated:
|
|||||||
changed_class_name: ~
|
changed_class_name: ~
|
||||||
changed_properties:
|
changed_properties:
|
||||||
Foo:
|
Foo:
|
||||||
Type: String
|
String: Value of Foo
|
||||||
Value: Value of Foo
|
|
||||||
changed_metadata: ~
|
changed_metadata: ~
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
---
|
---
|
||||||
source: src/snapshot/tests/apply.rs
|
source: src/snapshot/tests/apply.rs
|
||||||
expression: tree_view
|
expression: tree_view
|
||||||
|
|
||||||
---
|
---
|
||||||
id: id-1
|
id: id-1
|
||||||
name: ROOT
|
name: ROOT
|
||||||
class_name: ROOT
|
class_name: ROOT
|
||||||
properties:
|
properties:
|
||||||
Foo:
|
Foo:
|
||||||
Type: String
|
String: Value of Foo
|
||||||
Value: Value of Foo
|
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
context: {}
|
context: {}
|
||||||
children: []
|
children: []
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
---
|
---
|
||||||
source: src/snapshot/tests/apply.rs
|
source: src/snapshot/tests/apply.rs
|
||||||
expression: tree_view
|
expression: tree_view
|
||||||
|
|
||||||
---
|
---
|
||||||
id: id-1
|
id: id-1
|
||||||
name: ROOT
|
name: ROOT
|
||||||
class_name: ROOT
|
class_name: ROOT
|
||||||
properties:
|
properties:
|
||||||
Foo:
|
Foo:
|
||||||
Type: String
|
String: Should be removed
|
||||||
Value: Should be removed
|
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
context: {}
|
context: {}
|
||||||
children: []
|
children: []
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
source: src/snapshot/tests/compute.rs
|
source: src/snapshot/tests/compute.rs
|
||||||
expression: patch_value
|
expression: patch_value
|
||||||
|
|
||||||
---
|
---
|
||||||
removed_instances: []
|
removed_instances: []
|
||||||
added_instances: []
|
added_instances: []
|
||||||
@@ -10,6 +11,6 @@ updated_instances:
|
|||||||
changed_class_name: ~
|
changed_class_name: ~
|
||||||
changed_properties:
|
changed_properties:
|
||||||
PropertyName:
|
PropertyName:
|
||||||
Type: String
|
String: "Hello, world!"
|
||||||
Value: "Hello, world!"
|
|
||||||
changed_metadata: ~
|
changed_metadata: ~
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{HashMap, VecDeque},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use rbx_dom_weak::{Descendants, RbxId, RbxInstance, RbxInstanceProperties, RbxTree, RbxValue};
|
use rbx_dom_weak::{
|
||||||
|
types::{Ref, Variant},
|
||||||
|
Instance, InstanceBuilder, WeakDom,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::multimap::MultiMap;
|
use crate::multimap::MultiMap;
|
||||||
|
|
||||||
use super::InstanceMetadata;
|
use super::{InstanceMetadata, InstanceSnapshot};
|
||||||
|
|
||||||
/// An expanded variant of rbx_dom_weak's `RbxTree` that tracks additional
|
/// An expanded variant of rbx_dom_weak's `WeakDom` that tracks additional
|
||||||
/// metadata per instance that's Rojo-specific.
|
/// metadata per instance that's Rojo-specific.
|
||||||
///
|
///
|
||||||
/// This tree is also optimized for doing fast incremental updates and patches.
|
/// This tree is also optimized for doing fast incremental updates and patches.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RojoTree {
|
pub struct RojoTree {
|
||||||
/// Contains the instances without their Rojo-specific metadata.
|
/// Contains the instances without their Rojo-specific metadata.
|
||||||
inner: RbxTree,
|
inner: WeakDom,
|
||||||
|
|
||||||
/// Metadata associated with each instance that is kept up-to-date with the
|
/// Metadata associated with each instance that is kept up-to-date with the
|
||||||
/// set of actual instances.
|
/// set of actual instances.
|
||||||
metadata_map: HashMap<RbxId, InstanceMetadata>,
|
metadata_map: HashMap<Ref, InstanceMetadata>,
|
||||||
|
|
||||||
/// A multimap from source paths to all of the root instances that were
|
/// A multimap from source paths to all of the root instances that were
|
||||||
/// constructed from that path.
|
/// constructed from that path.
|
||||||
@@ -29,31 +32,42 @@ pub struct RojoTree {
|
|||||||
/// value portion of the map is also a set in order to support the same path
|
/// 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
|
/// appearing multiple times in the same Rojo project. This is sometimes
|
||||||
/// called "path aliasing" in various Rojo documentation.
|
/// called "path aliasing" in various Rojo documentation.
|
||||||
path_to_ids: MultiMap<PathBuf, RbxId>,
|
path_to_ids: MultiMap<PathBuf, Ref>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RojoTree {
|
impl RojoTree {
|
||||||
pub fn new(root: InstancePropertiesWithMeta) -> 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);
|
||||||
|
|
||||||
let mut tree = RojoTree {
|
let mut tree = RojoTree {
|
||||||
inner: RbxTree::new(root.properties),
|
inner: WeakDom::new(root_builder),
|
||||||
metadata_map: HashMap::new(),
|
metadata_map: HashMap::new(),
|
||||||
path_to_ids: MultiMap::new(),
|
path_to_ids: MultiMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
tree.insert_metadata(tree.inner.get_root_id(), root.metadata);
|
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
|
tree
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn inner(&self) -> &RbxTree {
|
pub fn inner(&self) -> &WeakDom {
|
||||||
&self.inner
|
&self.inner
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_root_id(&self) -> RbxId {
|
pub fn get_root_id(&self) -> Ref {
|
||||||
self.inner.get_root_id()
|
self.inner.root_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_instance(&self, id: RbxId) -> Option<InstanceWithMeta> {
|
pub fn get_instance(&self, id: Ref) -> Option<InstanceWithMeta> {
|
||||||
if let Some(instance) = self.inner.get_instance(id) {
|
if let Some(instance) = self.inner.get_by_ref(id) {
|
||||||
let metadata = self.metadata_map.get(&id).unwrap();
|
let metadata = self.metadata_map.get(&id).unwrap();
|
||||||
|
|
||||||
Some(InstanceWithMeta { instance, metadata })
|
Some(InstanceWithMeta { instance, metadata })
|
||||||
@@ -62,8 +76,8 @@ impl RojoTree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_instance_mut(&mut self, id: RbxId) -> Option<InstanceWithMetaMut> {
|
pub fn get_instance_mut(&mut self, id: Ref) -> Option<InstanceWithMetaMut> {
|
||||||
if let Some(instance) = self.inner.get_instance_mut(id) {
|
if let Some(instance) = self.inner.get_by_ref_mut(id) {
|
||||||
let metadata = self.metadata_map.get_mut(&id).unwrap();
|
let metadata = self.metadata_map.get_mut(&id).unwrap();
|
||||||
|
|
||||||
Some(InstanceWithMetaMut { instance, metadata })
|
Some(InstanceWithMetaMut { instance, metadata })
|
||||||
@@ -72,38 +86,38 @@ impl RojoTree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_instance(
|
pub fn insert_instance(&mut self, parent_ref: Ref, snapshot: InstanceSnapshot) -> Ref {
|
||||||
&mut self,
|
let builder = InstanceBuilder::new(snapshot.class_name.to_owned())
|
||||||
properties: InstancePropertiesWithMeta,
|
.with_name(snapshot.name.to_owned())
|
||||||
parent_id: RbxId,
|
.with_properties(snapshot.properties);
|
||||||
) -> RbxId {
|
|
||||||
let id = self.inner.insert_instance(properties.properties, parent_id);
|
let referent = self.inner.insert(parent_ref, builder);
|
||||||
self.insert_metadata(id, properties.metadata);
|
self.insert_metadata(referent, snapshot.metadata);
|
||||||
id
|
|
||||||
|
for child in snapshot.children {
|
||||||
|
self.insert_instance(referent, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
referent
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_instance(&mut self, id: RbxId) -> Option<RojoTree> {
|
pub fn remove(&mut self, id: Ref) {
|
||||||
if let Some(inner) = self.inner.remove_instance(id) {
|
let mut to_move = VecDeque::new();
|
||||||
let mut metadata_map = HashMap::new();
|
to_move.push_back(id);
|
||||||
let mut path_to_ids = MultiMap::new();
|
|
||||||
|
|
||||||
self.move_metadata(id, &mut metadata_map, &mut path_to_ids);
|
while let Some(id) = to_move.pop_front() {
|
||||||
for instance in inner.descendants(id) {
|
self.remove_metadata(id);
|
||||||
self.move_metadata(instance.get_id(), &mut metadata_map, &mut path_to_ids);
|
|
||||||
|
if let Some(instance) = self.inner.get_by_ref(id) {
|
||||||
|
to_move.extend(instance.children().iter().copied());
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(RojoTree {
|
|
||||||
inner,
|
|
||||||
metadata_map,
|
|
||||||
path_to_ids,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.inner.destroy(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces the metadata associated with the given instance ID.
|
/// Replaces the metadata associated with the given instance ID.
|
||||||
pub fn update_metadata(&mut self, id: RbxId, metadata: InstanceMetadata) {
|
pub fn update_metadata(&mut self, id: Ref, metadata: InstanceMetadata) {
|
||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
|
|
||||||
match self.metadata_map.entry(id) {
|
match self.metadata_map.entry(id) {
|
||||||
@@ -131,22 +145,22 @@ impl RojoTree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn descendants(&self, id: RbxId) -> RojoDescendants<'_> {
|
pub fn descendants(&self, id: Ref) -> RojoDescendants<'_> {
|
||||||
RojoDescendants {
|
let mut queue = VecDeque::new();
|
||||||
inner: self.inner.descendants(id),
|
queue.push_back(id);
|
||||||
tree: self,
|
|
||||||
}
|
RojoDescendants { queue, tree: self }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_ids_at_path(&self, path: &Path) -> &[RbxId] {
|
pub fn get_ids_at_path(&self, path: &Path) -> &[Ref] {
|
||||||
self.path_to_ids.get(path)
|
self.path_to_ids.get(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_metadata(&self, id: RbxId) -> Option<&InstanceMetadata> {
|
pub fn get_metadata(&self, id: Ref) -> Option<&InstanceMetadata> {
|
||||||
self.metadata_map.get(&id)
|
self.metadata_map.get(&id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_metadata(&mut self, id: RbxId, metadata: InstanceMetadata) {
|
fn insert_metadata(&mut self, id: Ref, metadata: InstanceMetadata) {
|
||||||
for path in &metadata.relevant_paths {
|
for path in &metadata.relevant_paths {
|
||||||
self.path_to_ids.insert(path.clone(), id);
|
self.path_to_ids.insert(path.clone(), id);
|
||||||
}
|
}
|
||||||
@@ -156,25 +170,17 @@ impl RojoTree {
|
|||||||
|
|
||||||
/// Moves the Rojo metadata from the instance with the given ID from this
|
/// Moves the Rojo metadata from the instance with the given ID from this
|
||||||
/// tree into some loose maps.
|
/// tree into some loose maps.
|
||||||
fn move_metadata(
|
fn remove_metadata(&mut self, id: Ref) {
|
||||||
&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();
|
let metadata = self.metadata_map.remove(&id).unwrap();
|
||||||
|
|
||||||
for path in &metadata.relevant_paths {
|
for path in &metadata.relevant_paths {
|
||||||
self.path_to_ids.remove(path, id);
|
self.path_to_ids.remove(path, id);
|
||||||
path_to_ids.insert(path.clone(), id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata_map.insert(id, metadata);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RojoDescendants<'a> {
|
pub struct RojoDescendants<'a> {
|
||||||
inner: Descendants<'a>,
|
queue: VecDeque<Ref>,
|
||||||
tree: &'a RojoTree,
|
tree: &'a RojoTree,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,50 +188,43 @@ impl<'a> Iterator for RojoDescendants<'a> {
|
|||||||
type Item = InstanceWithMeta<'a>;
|
type Item = InstanceWithMeta<'a>;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let instance = self.inner.next()?;
|
let id = self.queue.pop_front()?;
|
||||||
|
|
||||||
|
let instance = self
|
||||||
|
.tree
|
||||||
|
.inner
|
||||||
|
.get_by_ref(id)
|
||||||
|
.expect("Instance did not exist");
|
||||||
|
|
||||||
let metadata = self
|
let metadata = self
|
||||||
.tree
|
.tree
|
||||||
.get_metadata(instance.get_id())
|
.get_metadata(instance.referent())
|
||||||
.expect("Metadata did not exist for instance");
|
.expect("Metadata did not exist for instance");
|
||||||
|
|
||||||
|
self.queue.extend(instance.children().iter().copied());
|
||||||
|
|
||||||
Some(InstanceWithMeta { instance, metadata })
|
Some(InstanceWithMeta { instance, metadata })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RojoTree's equivalent of `RbxInstanceProperties`.
|
/// RojoTree's equivalent of `&'a Instance`.
|
||||||
#[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
|
/// This has to be a value type for RojoTree because the instance and metadata
|
||||||
/// are stored in different places. The mutable equivalent is
|
/// are stored in different places. The mutable equivalent is
|
||||||
/// `InstanceWithMetaMut`.
|
/// `InstanceWithMetaMut`.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct InstanceWithMeta<'a> {
|
pub struct InstanceWithMeta<'a> {
|
||||||
instance: &'a RbxInstance,
|
instance: &'a Instance,
|
||||||
metadata: &'a InstanceMetadata,
|
metadata: &'a InstanceMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> InstanceWithMeta<'a> {
|
impl<'a> InstanceWithMeta<'a> {
|
||||||
pub fn id(&self) -> RbxId {
|
pub fn id(&self) -> Ref {
|
||||||
self.instance.get_id()
|
self.instance.referent()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parent(&self) -> Option<RbxId> {
|
pub fn parent(&self) -> Ref {
|
||||||
self.instance.get_parent_id()
|
self.instance.parent()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(&self) -> &'a str {
|
pub fn name(&self) -> &'a str {
|
||||||
@@ -233,15 +232,15 @@ impl<'a> InstanceWithMeta<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn class_name(&self) -> &'a str {
|
pub fn class_name(&self) -> &'a str {
|
||||||
&self.instance.class_name
|
&self.instance.class
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn properties(&self) -> &'a HashMap<String, RbxValue> {
|
pub fn properties(&self) -> &'a HashMap<String, Variant> {
|
||||||
&self.instance.properties
|
&self.instance.properties
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn children(&self) -> &'a [RbxId] {
|
pub fn children(&self) -> &'a [Ref] {
|
||||||
self.instance.get_children_ids()
|
self.instance.children()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn metadata(&self) -> &'a InstanceMetadata {
|
pub fn metadata(&self) -> &'a InstanceMetadata {
|
||||||
@@ -249,20 +248,20 @@ impl<'a> InstanceWithMeta<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RojoTree's equivalent of `&'a mut RbxInstance`.
|
/// RojoTree's equivalent of `&'a mut Instance`.
|
||||||
///
|
///
|
||||||
/// This has to be a value type for RojoTree because the instance and metadata
|
/// This has to be a value type for RojoTree because the instance and metadata
|
||||||
/// are stored in different places. The immutable equivalent is
|
/// are stored in different places. The immutable equivalent is
|
||||||
/// `InstanceWithMeta`.
|
/// `InstanceWithMeta`.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InstanceWithMetaMut<'a> {
|
pub struct InstanceWithMetaMut<'a> {
|
||||||
instance: &'a mut RbxInstance,
|
instance: &'a mut Instance,
|
||||||
metadata: &'a mut InstanceMetadata,
|
metadata: &'a mut InstanceMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InstanceWithMetaMut<'_> {
|
impl InstanceWithMetaMut<'_> {
|
||||||
pub fn id(&self) -> RbxId {
|
pub fn id(&self) -> Ref {
|
||||||
self.instance.get_id()
|
self.instance.referent()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
pub fn name(&self) -> &str {
|
||||||
@@ -274,23 +273,23 @@ impl InstanceWithMetaMut<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn class_name(&self) -> &str {
|
pub fn class_name(&self) -> &str {
|
||||||
&self.instance.class_name
|
&self.instance.class
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn class_name_mut(&mut self) -> &mut String {
|
pub fn class_name_mut(&mut self) -> &mut String {
|
||||||
&mut self.instance.class_name
|
&mut self.instance.class
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn properties(&self) -> &HashMap<String, RbxValue> {
|
pub fn properties(&self) -> &HashMap<String, Variant> {
|
||||||
&self.instance.properties
|
&self.instance.properties
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn properties_mut(&mut self) -> &mut HashMap<String, RbxValue> {
|
pub fn properties_mut(&mut self) -> &mut HashMap<String, Variant> {
|
||||||
&mut self.instance.properties
|
&mut self.instance.properties
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn children(&self) -> &[RbxId] {
|
pub fn children(&self) -> &[Ref] {
|
||||||
self.instance.get_children_ids()
|
self.instance.children()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn metadata(&self) -> &InstanceMetadata {
|
pub fn metadata(&self) -> &InstanceMetadata {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ use std::{collections::BTreeMap, path::Path};
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use maplit::hashmap;
|
use maplit::hashmap;
|
||||||
use memofs::{IoResultExt, Vfs};
|
use memofs::{IoResultExt, Vfs};
|
||||||
use rbx_dom_weak::RbxValue;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||||
@@ -30,9 +29,7 @@ pub fn snapshot_csv(
|
|||||||
.name(instance_name)
|
.name(instance_name)
|
||||||
.class_name("LocalizationTable")
|
.class_name("LocalizationTable")
|
||||||
.properties(hashmap! {
|
.properties(hashmap! {
|
||||||
"Contents".to_owned() => RbxValue::String {
|
"Contents".to_owned() => table_contents.into(),
|
||||||
value: table_contents,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
@@ -41,8 +38,8 @@ pub fn snapshot_csv(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
|
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
|
||||||
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, &meta_path)?;
|
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
|
||||||
metadata.apply_all(&mut snapshot);
|
metadata.apply_all(&mut snapshot)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(snapshot))
|
Ok(Some(snapshot))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user