forked from rojo-rbx/rojo
Compare commits
30 Commits
v7.0.0-alp
...
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 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
rust_version: [stable, "1.43.1"]
|
||||
rust_version: [stable, "1.46.0"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
53
CHANGELOG.md
53
CHANGELOG.md
@@ -2,6 +2,59 @@
|
||||
|
||||
## 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.
|
||||
|
||||
985
Cargo.lock
generated
985
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
40
Cargo.toml
40
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rojo"
|
||||
version = "7.0.0-alpha.4"
|
||||
version = "7.0.0-rc.1"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
description = "Enables professional-grade development tools for Roblox developers"
|
||||
license = "MPL-2.0"
|
||||
@@ -9,6 +9,7 @@ documentation = "https://rojo.space/docs"
|
||||
repository = "https://github.com/rojo-rbx/rojo"
|
||||
readme = "README.md"
|
||||
edition = "2018"
|
||||
build = "build.rs"
|
||||
|
||||
exclude = [
|
||||
"/test-projects/**",
|
||||
@@ -45,7 +46,7 @@ name = "build"
|
||||
harness = false
|
||||
|
||||
[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" }
|
||||
@@ -54,48 +55,49 @@ memofs = { version = "0.1.2", path = "memofs" }
|
||||
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
||||
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
||||
|
||||
rbx_binary = "0.6.0-alpha.1"
|
||||
rbx_dom_weak = "2.0.0-alpha.1"
|
||||
rbx_reflection = "4.0.0-alpha.1"
|
||||
rbx_reflection_database = "0.1.0"
|
||||
rbx_xml = "0.12.0-alpha.1"
|
||||
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"
|
||||
backtrace = "0.3"
|
||||
bincode = "1.2.1"
|
||||
crossbeam-channel = "0.4.0"
|
||||
crossbeam-channel = "0.5.1"
|
||||
csv = "1.1.1"
|
||||
env_logger = "0.7.1"
|
||||
env_logger = "0.9.0"
|
||||
fs-err = "2.2.0"
|
||||
futures = "0.1.29"
|
||||
futures = "0.3.16"
|
||||
globset = "0.4.4"
|
||||
humantime = "1.3.0"
|
||||
hyper = "0.12.35"
|
||||
humantime = "2.1.0"
|
||||
hyper = { version = "0.14.11", features = ["server", "tcp", "http1"] }
|
||||
jod-thread = "0.1.0"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.8"
|
||||
maplit = "1.0.1"
|
||||
notify = "4.0.14"
|
||||
opener = "0.4.1"
|
||||
opener = "0.5.0"
|
||||
regex = "1.3.1"
|
||||
reqwest = "0.9.20"
|
||||
ritz = "0.1.0"
|
||||
rlua = "0.17.0"
|
||||
roblox_install = "0.2.2"
|
||||
roblox_install = "1.0.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_json = "1.0"
|
||||
structopt = "0.3.5"
|
||||
termcolor = "1.0.5"
|
||||
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"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.6.2"
|
||||
winreg = "0.9.0"
|
||||
|
||||
[build-dependencies]
|
||||
memofs = { version = "0.1.3", path = "memofs" }
|
||||
memofs = { version = "0.2.0", path = "memofs" }
|
||||
|
||||
embed-resource = "1.6"
|
||||
anyhow = "1.0.27"
|
||||
bincode = "1.2.1"
|
||||
fs-err = "2.3.0"
|
||||
@@ -107,8 +109,8 @@ rojo-insta-ext = { path = "rojo-insta-ext" }
|
||||
criterion = "0.3"
|
||||
insta = { version = "1.3.0", features = ["redactions"] }
|
||||
lazy_static = "1.2"
|
||||
paste = "0.1"
|
||||
pretty_assertions = "0.6.1"
|
||||
paste = "1.0.5"
|
||||
pretty_assertions = "0.7.2"
|
||||
serde_yaml = "0.8.9"
|
||||
tempfile = "3.0"
|
||||
walkdir = "2.1"
|
||||
|
||||
18
README.md
18
README.md
@@ -1,21 +1,13 @@
|
||||
<div align="center">
|
||||
<a href="https://rojo.space">
|
||||
<img src="assets/logo-512.png" alt="Rojo" height="217" />
|
||||
</a>
|
||||
<a href="https://rojo.space"><img src="assets/logo-512.png" alt="Rojo" height="217" /></a>
|
||||
</div>
|
||||
|
||||
<div> </div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/rojo-rbx/rojo/actions">
|
||||
<img src="https://github.com/rojo-rbx/rojo/workflows/CI/badge.svg" alt="Actions status" />
|
||||
</a>
|
||||
<a href="https://crates.io/crates/rojo">
|
||||
<img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" />
|
||||
</a>
|
||||
<a href="https://rojo.space/docs">
|
||||
<img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" />
|
||||
</a>
|
||||
<a href="https://github.com/rojo-rbx/rojo/actions"><img src="https://github.com/rojo-rbx/rojo/workflows/CI/badge.svg" alt="Actions status" /></a>
|
||||
<a href="https://crates.io/crates/rojo"><img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" /></a>
|
||||
<a href="https://rojo.space/docs"><img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" /></a>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
@@ -48,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
|
||||
|
||||
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
|
||||
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)?;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
## Unreleased Changes
|
||||
|
||||
## 0.2.0 (2021-08-23)
|
||||
* Updated to `crossbeam-channel` 0.5.1.
|
||||
|
||||
## 0.1.3 (2020-11-19)
|
||||
* Added `set_watch_enabled` to `Vfs` and `VfsLock` to allow turning off file watching.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "memofs"
|
||||
description = "Virtual filesystem with configurable backends."
|
||||
version = "0.1.3"
|
||||
version = "0.2.0"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
@@ -11,7 +11,7 @@ homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
crossbeam-channel = "0.4.0"
|
||||
crossbeam-channel = "0.5.1"
|
||||
fs-err = "2.3.0"
|
||||
notify = "4.0.15"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@@ -71,8 +71,8 @@ types = {
|
||||
|
||||
CFrame = {
|
||||
fromPod = function(pod)
|
||||
local pos = pod.Position
|
||||
local orient = pod.Orientation
|
||||
local pos = pod.position
|
||||
local orient = pod.orientation
|
||||
|
||||
return CFrame.new(
|
||||
pos[1], pos[2], pos[3],
|
||||
@@ -89,8 +89,8 @@ types = {
|
||||
r20, r21, r22 = roblox:GetComponents()
|
||||
|
||||
return {
|
||||
Position = {x, y, z},
|
||||
Orientation = {
|
||||
position = {x, y, z},
|
||||
orientation = {
|
||||
{r00, r01, r02},
|
||||
{r10, r11, r12},
|
||||
{r20, r21, r22},
|
||||
@@ -123,10 +123,10 @@ types = {
|
||||
fromPod = function(pod)
|
||||
local keypoints = {}
|
||||
|
||||
for index, keypoint in ipairs(pod.Keypoints) do
|
||||
for index, keypoint in ipairs(pod.keypoints) do
|
||||
keypoints[index] = ColorSequenceKeypoint.new(
|
||||
keypoint.Time,
|
||||
types.Color3.fromPod(keypoint.Color)
|
||||
keypoint.time,
|
||||
types.Color3.fromPod(keypoint.color)
|
||||
)
|
||||
end
|
||||
|
||||
@@ -138,13 +138,13 @@ types = {
|
||||
|
||||
for index, keypoint in ipairs(roblox.Keypoints) do
|
||||
keypoints[index] = {
|
||||
Time = keypoint.Time,
|
||||
Color = types.Color3.toPod(keypoint.Value),
|
||||
time = keypoint.Time,
|
||||
color = types.Color3.toPod(keypoint.Value),
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
Keypoints = keypoints,
|
||||
keypoints = keypoints,
|
||||
}
|
||||
end,
|
||||
},
|
||||
@@ -223,11 +223,11 @@ types = {
|
||||
fromPod = function(pod)
|
||||
local keypoints = {}
|
||||
|
||||
for index, keypoint in ipairs(pod.Keypoints) do
|
||||
for index, keypoint in ipairs(pod.keypoints) do
|
||||
keypoints[index] = NumberSequenceKeypoint.new(
|
||||
keypoint.Time,
|
||||
keypoint.Value,
|
||||
keypoint.Envelope
|
||||
keypoint.time,
|
||||
keypoint.value,
|
||||
keypoint.envelope
|
||||
)
|
||||
end
|
||||
|
||||
@@ -239,14 +239,14 @@ types = {
|
||||
|
||||
for index, keypoint in ipairs(roblox.Keypoints) do
|
||||
keypoints[index] = {
|
||||
Time = keypoint.Time,
|
||||
Value = keypoint.Value,
|
||||
Envelope = keypoint.Envelope,
|
||||
time = keypoint.Time,
|
||||
value = keypoint.Value,
|
||||
envelope = keypoint.Envelope,
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
Keypoints = keypoints,
|
||||
keypoints = keypoints,
|
||||
}
|
||||
end,
|
||||
},
|
||||
@@ -257,11 +257,11 @@ types = {
|
||||
return nil
|
||||
else
|
||||
return PhysicalProperties.new(
|
||||
pod.Density,
|
||||
pod.Friction,
|
||||
pod.Elasticity,
|
||||
pod.FrictionWeight,
|
||||
pod.ElasticityWeight
|
||||
pod.density,
|
||||
pod.friction,
|
||||
pod.elasticity,
|
||||
pod.frictionWeight,
|
||||
pod.elasticityWeight
|
||||
)
|
||||
end
|
||||
end,
|
||||
@@ -271,11 +271,11 @@ types = {
|
||||
return "Default"
|
||||
else
|
||||
return {
|
||||
Density = roblox.Density,
|
||||
Friction = roblox.Friction,
|
||||
Elasticity = roblox.Elasticity,
|
||||
FrictionWeight = roblox.FrictionWeight,
|
||||
ElasticityWeight = roblox.ElasticityWeight,
|
||||
density = roblox.Density,
|
||||
friction = roblox.Friction,
|
||||
elasticity = roblox.Elasticity,
|
||||
frictionWeight = roblox.FrictionWeight,
|
||||
elasticityWeight = roblox.ElasticityWeight,
|
||||
}
|
||||
end
|
||||
end,
|
||||
@@ -284,15 +284,15 @@ types = {
|
||||
Ray = {
|
||||
fromPod = function(pod)
|
||||
return Ray.new(
|
||||
types.Vector3.fromPod(pod.Origin),
|
||||
types.Vector3.fromPod(pod.Direction)
|
||||
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),
|
||||
origin = types.Vector3.toPod(roblox.Origin),
|
||||
direction = types.Vector3.toPod(roblox.Direction),
|
||||
}
|
||||
end,
|
||||
},
|
||||
@@ -431,12 +431,14 @@ types = {
|
||||
local EncodedValue = {}
|
||||
|
||||
function EncodedValue.decode(encodedValue)
|
||||
local typeImpl = types[encodedValue.Type]
|
||||
local ty, value = next(encodedValue)
|
||||
|
||||
local typeImpl = types[ty]
|
||||
if typeImpl == nil then
|
||||
return false, "Couldn't decode value " .. tostring(encodedValue.Type)
|
||||
return false, "Couldn't decode value " .. tostring(ty)
|
||||
end
|
||||
|
||||
return true, typeImpl.fromPod(encodedValue.Value)
|
||||
return true, typeImpl.fromPod(value)
|
||||
end
|
||||
|
||||
function EncodedValue.encode(rbxValue, propertyType)
|
||||
@@ -448,8 +450,7 @@ function EncodedValue.encode(rbxValue, propertyType)
|
||||
end
|
||||
|
||||
return true, {
|
||||
Type = propertyType,
|
||||
Value = typeImpl.toPod(rbxValue),
|
||||
[propertyType] = typeImpl.toPod(rbxValue),
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"Axes": {
|
||||
"value": {
|
||||
"Type": "Axes",
|
||||
"Value": [
|
||||
"Axes": [
|
||||
"X",
|
||||
"Y",
|
||||
"Z"
|
||||
@@ -12,35 +11,31 @@
|
||||
},
|
||||
"BinaryString": {
|
||||
"value": {
|
||||
"Type": "BinaryString",
|
||||
"Value": "SGVsbG8h"
|
||||
"BinaryString": "SGVsbG8h"
|
||||
},
|
||||
"ty": "BinaryString"
|
||||
},
|
||||
"Bool": {
|
||||
"value": {
|
||||
"Type": "Bool",
|
||||
"Value": true
|
||||
"Bool": true
|
||||
},
|
||||
"ty": "Bool"
|
||||
},
|
||||
"BrickColor": {
|
||||
"value": {
|
||||
"Type": "BrickColor",
|
||||
"Value": 1004
|
||||
"BrickColor": 1004
|
||||
},
|
||||
"ty": "BrickColor"
|
||||
},
|
||||
"CFrame": {
|
||||
"value": {
|
||||
"Type": "CFrame",
|
||||
"Value": {
|
||||
"Position": [
|
||||
"CFrame": {
|
||||
"position": [
|
||||
1.0,
|
||||
2.0,
|
||||
3.0
|
||||
],
|
||||
"Orientation": [
|
||||
"orientation": [
|
||||
[
|
||||
4.0,
|
||||
5.0,
|
||||
@@ -63,8 +58,7 @@
|
||||
},
|
||||
"Color3": {
|
||||
"value": {
|
||||
"Type": "Color3",
|
||||
"Value": [
|
||||
"Color3": [
|
||||
1.0,
|
||||
2.0,
|
||||
3.0
|
||||
@@ -74,8 +68,7 @@
|
||||
},
|
||||
"Color3uint8": {
|
||||
"value": {
|
||||
"Type": "Color3uint8",
|
||||
"Value": [
|
||||
"Color3uint8": [
|
||||
0,
|
||||
128,
|
||||
255
|
||||
@@ -85,20 +78,19 @@
|
||||
},
|
||||
"ColorSequence": {
|
||||
"value": {
|
||||
"Type": "ColorSequence",
|
||||
"Value": {
|
||||
"Keypoints": [
|
||||
"ColorSequence": {
|
||||
"keypoints": [
|
||||
{
|
||||
"Time": 0.0,
|
||||
"Color": [
|
||||
"time": 0.0,
|
||||
"color": [
|
||||
1.0,
|
||||
1.0,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"Time": 1.0,
|
||||
"Color": [
|
||||
"time": 1.0,
|
||||
"color": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
@@ -111,22 +103,19 @@
|
||||
},
|
||||
"Content": {
|
||||
"value": {
|
||||
"Type": "Content",
|
||||
"Value": "rbxassetid://12345"
|
||||
"Content": "rbxassetid://12345"
|
||||
},
|
||||
"ty": "Content"
|
||||
},
|
||||
"Enum": {
|
||||
"value": {
|
||||
"Type": "Enum",
|
||||
"Value": 1234
|
||||
"Enum": 1234
|
||||
},
|
||||
"ty": "Enum"
|
||||
},
|
||||
"Faces": {
|
||||
"value": {
|
||||
"Type": "Faces",
|
||||
"Value": [
|
||||
"Faces": [
|
||||
"Right",
|
||||
"Top",
|
||||
"Back",
|
||||
@@ -139,36 +128,31 @@
|
||||
},
|
||||
"Float32": {
|
||||
"value": {
|
||||
"Type": "Float32",
|
||||
"Value": 15.0
|
||||
"Float32": 15.0
|
||||
},
|
||||
"ty": "Float32"
|
||||
},
|
||||
"Float64": {
|
||||
"value": {
|
||||
"Type": "Float64",
|
||||
"Value": 15123.0
|
||||
"Float64": 15123.0
|
||||
},
|
||||
"ty": "Float64"
|
||||
},
|
||||
"Int32": {
|
||||
"value": {
|
||||
"Type": "Int32",
|
||||
"Value": 6014
|
||||
"Int32": 6014
|
||||
},
|
||||
"ty": "Int32"
|
||||
},
|
||||
"Int64": {
|
||||
"value": {
|
||||
"Type": "Int64",
|
||||
"Value": 23491023
|
||||
"Int64": 23491023
|
||||
},
|
||||
"ty": "Int64"
|
||||
},
|
||||
"NumberRange": {
|
||||
"value": {
|
||||
"Type": "NumberRange",
|
||||
"Value": [
|
||||
"NumberRange": [
|
||||
-36.0,
|
||||
94.0
|
||||
]
|
||||
@@ -177,18 +161,17 @@
|
||||
},
|
||||
"NumberSequence": {
|
||||
"value": {
|
||||
"Type": "NumberSequence",
|
||||
"Value": {
|
||||
"Keypoints": [
|
||||
"NumberSequence": {
|
||||
"keypoints": [
|
||||
{
|
||||
"Time": 0.0,
|
||||
"Value": 5.0,
|
||||
"Envelope": 2.0
|
||||
"time": 0.0,
|
||||
"value": 5.0,
|
||||
"envelope": 2.0
|
||||
},
|
||||
{
|
||||
"Time": 1.0,
|
||||
"Value": 22.0,
|
||||
"Envelope": 0.0
|
||||
"time": 1.0,
|
||||
"value": 22.0,
|
||||
"envelope": 0.0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -197,34 +180,31 @@
|
||||
},
|
||||
"PhysicalProperties-Custom": {
|
||||
"value": {
|
||||
"Type": "PhysicalProperties",
|
||||
"Value": {
|
||||
"Density": 0.5,
|
||||
"Friction": 1.0,
|
||||
"Elasticity": 0.0,
|
||||
"FrictionWeight": 50.0,
|
||||
"ElasticityWeight": 25.0
|
||||
"PhysicalProperties": {
|
||||
"density": 0.5,
|
||||
"friction": 1.0,
|
||||
"elasticity": 0.0,
|
||||
"frictionWeight": 50.0,
|
||||
"elasticityWeight": 25.0
|
||||
}
|
||||
},
|
||||
"ty": "PhysicalProperties"
|
||||
},
|
||||
"PhysicalProperties-Default": {
|
||||
"value": {
|
||||
"Type": "PhysicalProperties",
|
||||
"Value": "Default"
|
||||
"PhysicalProperties": "Default"
|
||||
},
|
||||
"ty": "PhysicalProperties"
|
||||
},
|
||||
"Ray": {
|
||||
"value": {
|
||||
"Type": "Ray",
|
||||
"Value": {
|
||||
"Origin": [
|
||||
"Ray": {
|
||||
"origin": [
|
||||
1.0,
|
||||
2.0,
|
||||
3.0
|
||||
],
|
||||
"Direction": [
|
||||
"direction": [
|
||||
4.0,
|
||||
5.0,
|
||||
6.0
|
||||
@@ -235,8 +215,7 @@
|
||||
},
|
||||
"Rect": {
|
||||
"value": {
|
||||
"Type": "Rect",
|
||||
"Value": [
|
||||
"Rect": [
|
||||
[
|
||||
0.0,
|
||||
5.0
|
||||
@@ -251,8 +230,7 @@
|
||||
},
|
||||
"Region3int16": {
|
||||
"value": {
|
||||
"Type": "Region3int16",
|
||||
"Value": [
|
||||
"Region3int16": [
|
||||
[
|
||||
-10,
|
||||
-5,
|
||||
@@ -269,15 +247,13 @@
|
||||
},
|
||||
"String": {
|
||||
"value": {
|
||||
"Type": "String",
|
||||
"Value": "Hello, world!"
|
||||
"String": "Hello, world!"
|
||||
},
|
||||
"ty": "String"
|
||||
},
|
||||
"UDim": {
|
||||
"value": {
|
||||
"Type": "UDim",
|
||||
"Value": [
|
||||
"UDim": [
|
||||
1.0,
|
||||
32
|
||||
]
|
||||
@@ -286,8 +262,7 @@
|
||||
},
|
||||
"UDim2": {
|
||||
"value": {
|
||||
"Type": "UDim2",
|
||||
"Value": [
|
||||
"UDim2": [
|
||||
[
|
||||
-1.0,
|
||||
100
|
||||
@@ -302,8 +277,7 @@
|
||||
},
|
||||
"Vector2": {
|
||||
"value": {
|
||||
"Type": "Vector2",
|
||||
"Value": [
|
||||
"Vector2": [
|
||||
-50.0,
|
||||
50.0
|
||||
]
|
||||
@@ -312,8 +286,7 @@
|
||||
},
|
||||
"Vector2int16": {
|
||||
"value": {
|
||||
"Type": "Vector2int16",
|
||||
"Value": [
|
||||
"Vector2int16": [
|
||||
-300,
|
||||
300
|
||||
]
|
||||
@@ -322,8 +295,7 @@
|
||||
},
|
||||
"Vector3": {
|
||||
"value": {
|
||||
"Type": "Vector3",
|
||||
"Value": [
|
||||
"Vector3": [
|
||||
-300.0,
|
||||
0.0,
|
||||
1500.0
|
||||
@@ -333,8 +305,7 @@
|
||||
},
|
||||
"Vector3int16": {
|
||||
"value": {
|
||||
"Type": "Vector3int16",
|
||||
"Value": [
|
||||
"Vector3int16": [
|
||||
60,
|
||||
37,
|
||||
-450
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ local function findCanonicalPropertyDescriptor(className, propertyName)
|
||||
local aliasData = propertyData.Kind.Alias
|
||||
if aliasData ~= nil then
|
||||
return PropertyDescriptor.fromRaw(
|
||||
currentClass.properties[aliasData.AliasFor],
|
||||
currentClass.Properties[aliasData.AliasFor],
|
||||
currentClassName,
|
||||
aliasData.AliasFor)
|
||||
end
|
||||
@@ -66,4 +66,4 @@ return {
|
||||
findCanonicalPropertyDescriptor = findCanonicalPropertyDescriptor,
|
||||
Error = Error,
|
||||
EncodedValue = require(script.EncodedValue),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
|
||||
return strict("Config", {
|
||||
isDevBuild = isDevBuild,
|
||||
codename = "Epiphany",
|
||||
version = {7, 0, 0, "-alpha.4"},
|
||||
version = {7, 0, 0, "-rc.1"},
|
||||
expectedServerVersionString = "7.0 or newer",
|
||||
protocolVersion = 4,
|
||||
defaultHost = "localhost",
|
||||
|
||||
@@ -146,8 +146,7 @@ return function()
|
||||
id = "VALUE",
|
||||
changedProperties = {
|
||||
Value = {
|
||||
Type = "String",
|
||||
Value = "WORLD",
|
||||
String = "WORLD",
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -176,8 +175,7 @@ return function()
|
||||
changedClassName = "StringValue",
|
||||
changedProperties = {
|
||||
Value = {
|
||||
Type = "String",
|
||||
Value = "I am Root",
|
||||
String = "I am Root",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -6,29 +6,31 @@
|
||||
local RbxDom = require(script.Parent.Parent.Parent.RbxDom)
|
||||
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.
|
||||
if virtualValue.Type == "Ref" then
|
||||
if virtualValue.Value == nil then
|
||||
if ty == "Ref" then
|
||||
if value == "00000000000000000000000000000000" then
|
||||
return true, nil
|
||||
end
|
||||
|
||||
local instance = instanceMap.fromIds[virtualValue.Value]
|
||||
local instance = instanceMap.fromIds[value]
|
||||
|
||||
if instance ~= nil then
|
||||
return true, instance
|
||||
else
|
||||
return false, Error.new(Error.RefDidNotExist, {
|
||||
virtualValue = virtualValue,
|
||||
encodedValue = encodedValue,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local ok, decodedValue = RbxDom.EncodedValue.decode(virtualValue)
|
||||
local ok, decodedValue = RbxDom.EncodedValue.decode(encodedValue)
|
||||
|
||||
if not ok then
|
||||
return false, Error.new(Error.CannotDecodeValue, {
|
||||
virtualValue = virtualValue,
|
||||
encodedValue = encodedValue,
|
||||
innerError = decodedValue,
|
||||
})
|
||||
end
|
||||
|
||||
@@ -75,7 +75,9 @@ local function diff(instanceMap, virtualInstances, rootId)
|
||||
changedProperties[propertyName] = virtualValue
|
||||
end
|
||||
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
|
||||
else
|
||||
local err = existingValueOrErr
|
||||
|
||||
@@ -80,8 +80,7 @@ return function()
|
||||
Name = "Value",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "String",
|
||||
Value = "Hello, world!",
|
||||
String = "Hello, world!",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
@@ -107,8 +106,9 @@ return function()
|
||||
|
||||
local patchProperty = update.changedProperties["Value"]
|
||||
expect(patchProperty).to.be.a("table")
|
||||
expect(patchProperty.Type).to.equal("String")
|
||||
expect(patchProperty.Value).to.equal("Hello, world!")
|
||||
local ty, value = next(patchProperty)
|
||||
expect(ty).to.equal("String")
|
||||
expect(value).to.equal("Hello, world!")
|
||||
end)
|
||||
|
||||
it("should generate an empty patch if no properties changed", function()
|
||||
@@ -119,8 +119,7 @@ return function()
|
||||
Name = "Value",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "String",
|
||||
Value = "Hello, world!",
|
||||
String = "Hello, world!",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
@@ -145,8 +144,7 @@ return function()
|
||||
Name = "Folder",
|
||||
Properties = {
|
||||
FAKE_PROPERTY = {
|
||||
Type = "String",
|
||||
Value = "Hello, world!",
|
||||
String = "Hello, world!",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
@@ -183,8 +181,7 @@ return function()
|
||||
-- heat_xml is a serialization-only property that is not
|
||||
-- exposed to Lua.
|
||||
heat_xml = {
|
||||
Type = "Float32",
|
||||
Value = 5,
|
||||
Float32 = 5,
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
|
||||
@@ -70,7 +70,7 @@ function reifyInner(instanceMap, virtualInstances, id, parentInstance, unapplied
|
||||
for propertyName, virtualValue in pairs(virtualInstance.Properties) do
|
||||
-- Because refs may refer to instances that we haven't constructed yet,
|
||||
-- we defer applying any ref properties until all instances are created.
|
||||
if virtualValue.Type == "Ref" then
|
||||
if next(virtualValue) == "Ref" then
|
||||
table.insert(deferredRefs, {
|
||||
id = id,
|
||||
instance = instance,
|
||||
@@ -136,23 +136,23 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
end
|
||||
|
||||
for _, entry in ipairs(deferredRefs) do
|
||||
local virtualValue = entry.virtualValue
|
||||
local _, refId = next(entry.virtualValue)
|
||||
|
||||
if virtualValue.Value == nil then
|
||||
if refId == nil then
|
||||
continue
|
||||
end
|
||||
|
||||
local targetInstance = instanceMap.fromIds[virtualValue.Value]
|
||||
local targetInstance = instanceMap.fromIds[refId]
|
||||
if targetInstance == nil then
|
||||
markFailed(entry.id, entry.propertyName, virtualValue)
|
||||
markFailed(entry.id, entry.propertyName, entry.virtualValue)
|
||||
continue
|
||||
end
|
||||
|
||||
local ok = setProperty(entry.instance, entry.propertyName, targetInstance)
|
||||
if not ok then
|
||||
markFailed(entry.id, entry.propertyName, virtualValue)
|
||||
markFailed(entry.id, entry.propertyName, entry.virtualValue)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return reify
|
||||
return reify
|
||||
|
||||
@@ -54,8 +54,7 @@ return function()
|
||||
Name = "Spaghetti",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "String",
|
||||
Value = "Hello, world!",
|
||||
String = "Hello, world!",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
@@ -191,8 +190,7 @@ return function()
|
||||
Name = "Child",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "Ref",
|
||||
Value = "ROOT",
|
||||
Ref = "ROOT",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
@@ -219,8 +217,7 @@ return function()
|
||||
Name = "Root",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "Ref",
|
||||
Value = "EXISTING",
|
||||
Ref = "EXISTING",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
@@ -258,8 +255,7 @@ return function()
|
||||
Name = "Child A",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "Ref",
|
||||
Value = "Child B",
|
||||
Ref = "Child B",
|
||||
},
|
||||
},
|
||||
Children = {},
|
||||
@@ -298,8 +294,7 @@ return function()
|
||||
Name = "Root",
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "Ref",
|
||||
Value = "CHILD",
|
||||
Ref = "CHILD",
|
||||
},
|
||||
},
|
||||
Children = {"CHILD"},
|
||||
|
||||
@@ -5,10 +5,7 @@ local strict = require(script.Parent.strict)
|
||||
|
||||
local RbxId = t.string
|
||||
|
||||
local ApiValue = t.interface({
|
||||
Type = t.string,
|
||||
Value = t.optional(t.any),
|
||||
})
|
||||
local ApiValue = t.keys(t.string)
|
||||
|
||||
local ApiInstanceMetadata = t.interface({
|
||||
ignoreUnknownInstances = t.optional(t.boolean),
|
||||
@@ -96,4 +93,4 @@ return strict("Types", {
|
||||
VirtualInstance = ApiInstance,
|
||||
VirtualMetadata = ApiInstanceMetadata,
|
||||
VirtualValue = ApiValue,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: tests/tests/build.rs
|
||||
expression: contents
|
||||
|
||||
---
|
||||
<roblox version="4">
|
||||
<Item class="DataModel" referent="0">
|
||||
@@ -31,11 +32,7 @@ expression: contents
|
||||
<Item class="Part" referent="4">
|
||||
<Properties>
|
||||
<string name="Name">Color</string>
|
||||
<Color3 name="Color3uint8">
|
||||
<R>0.5</R>
|
||||
<G>0.25</G>
|
||||
<B>0</B>
|
||||
</Color3>
|
||||
<Color3uint8 name="Color3uint8">8404992</Color3uint8>
|
||||
</Properties>
|
||||
</Item>
|
||||
<Item class="NumberValue" referent="5">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||
|
||||
---
|
||||
instances:
|
||||
id-2:
|
||||
@@ -13,7 +14,7 @@ instances:
|
||||
Parent: "00000000000000000000000000000000"
|
||||
Properties:
|
||||
Source:
|
||||
Type: String
|
||||
Value: "-- Edited contents"
|
||||
String: "-- Edited contents"
|
||||
messageCursor: 1
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||
|
||||
---
|
||||
instances:
|
||||
id-2:
|
||||
@@ -13,7 +14,7 @@ instances:
|
||||
Parent: "00000000000000000000000000000000"
|
||||
Properties:
|
||||
Source:
|
||||
Type: String
|
||||
Value: "-- Original contents"
|
||||
String: "-- Original contents"
|
||||
messageCursor: 0
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
@@ -12,7 +13,7 @@ messages:
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Source:
|
||||
Type: String
|
||||
Value: "-- Edited contents"
|
||||
String: "-- Edited contents"
|
||||
id: id-2
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||
|
||||
---
|
||||
instances:
|
||||
id-10:
|
||||
@@ -13,8 +14,7 @@ instances:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #6"
|
||||
String: "File #6"
|
||||
id-11:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -25,8 +25,7 @@ instances:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #7"
|
||||
String: "File #7"
|
||||
id-12:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -37,8 +36,7 @@ instances:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #8"
|
||||
String: "File #8"
|
||||
id-13:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -49,8 +47,7 @@ instances:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #9"
|
||||
String: "File #9"
|
||||
id-2:
|
||||
Children:
|
||||
- id-3
|
||||
@@ -90,8 +87,7 @@ instances:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #0"
|
||||
String: "File #0"
|
||||
id-5:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -102,8 +98,7 @@ instances:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #1"
|
||||
String: "File #1"
|
||||
id-6:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -114,8 +109,7 @@ instances:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #2"
|
||||
String: "File #2"
|
||||
id-7:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -126,8 +120,7 @@ instances:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #3"
|
||||
String: "File #3"
|
||||
id-8:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -138,8 +131,7 @@ instances:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #4"
|
||||
String: "File #4"
|
||||
id-9:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -150,7 +142,7 @@ instances:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #5"
|
||||
String: "File #5"
|
||||
messageCursor: 1
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
@@ -15,8 +16,7 @@ messages:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #6"
|
||||
String: "File #6"
|
||||
id-11:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -27,8 +27,7 @@ messages:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #7"
|
||||
String: "File #7"
|
||||
id-12:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -39,8 +38,7 @@ messages:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #8"
|
||||
String: "File #8"
|
||||
id-13:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -51,8 +49,7 @@ messages:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #9"
|
||||
String: "File #9"
|
||||
id-3:
|
||||
Children:
|
||||
- id-4
|
||||
@@ -82,8 +79,7 @@ messages:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #0"
|
||||
String: "File #0"
|
||||
id-5:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -94,8 +90,7 @@ messages:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #1"
|
||||
String: "File #1"
|
||||
id-6:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -106,8 +101,7 @@ messages:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #2"
|
||||
String: "File #2"
|
||||
id-7:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -118,8 +112,7 @@ messages:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #3"
|
||||
String: "File #3"
|
||||
id-8:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -130,8 +123,7 @@ messages:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #4"
|
||||
String: "File #4"
|
||||
id-9:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
@@ -142,8 +134,8 @@ messages:
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "File #5"
|
||||
String: "File #5"
|
||||
removed: []
|
||||
updated: []
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||
|
||||
---
|
||||
instances:
|
||||
id-2:
|
||||
@@ -23,7 +24,7 @@ instances:
|
||||
Parent: id-2
|
||||
Properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: This file will be removed!
|
||||
String: This file will be removed!
|
||||
messageCursor: 0
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||
|
||||
---
|
||||
instances:
|
||||
id-2:
|
||||
@@ -24,8 +25,7 @@ instances:
|
||||
Parent: id-2
|
||||
Properties:
|
||||
Source:
|
||||
Type: String
|
||||
Value: "-- Hello, from bar!"
|
||||
String: "-- Hello, from bar!"
|
||||
id-4:
|
||||
Children: []
|
||||
ClassName: ModuleScript
|
||||
@@ -36,7 +36,7 @@ instances:
|
||||
Parent: id-2
|
||||
Properties:
|
||||
Source:
|
||||
Type: String
|
||||
Value: Updated foo!
|
||||
String: Updated foo!
|
||||
messageCursor: 1
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||
|
||||
---
|
||||
instances:
|
||||
id-2:
|
||||
@@ -24,8 +25,7 @@ instances:
|
||||
Parent: id-2
|
||||
Properties:
|
||||
Source:
|
||||
Type: String
|
||||
Value: "-- Hello, from bar!"
|
||||
String: "-- Hello, from bar!"
|
||||
id-4:
|
||||
Children: []
|
||||
ClassName: ModuleScript
|
||||
@@ -36,7 +36,7 @@ instances:
|
||||
Parent: id-2
|
||||
Properties:
|
||||
Source:
|
||||
Type: String
|
||||
Value: "-- Hello, from foo!"
|
||||
String: "-- Hello, from foo!"
|
||||
messageCursor: 0
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
@@ -12,7 +13,7 @@ messages:
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Source:
|
||||
Type: String
|
||||
Value: Updated foo!
|
||||
String: Updated foo!
|
||||
id: id-4
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
17
src/bin.rs
17
src/bin.rs
@@ -3,20 +3,7 @@ use std::{env, panic, process};
|
||||
use backtrace::Backtrace;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use librojo::cli::{self, GlobalOptions, Options, Subcommand};
|
||||
|
||||
fn run(global: GlobalOptions, subcommand: Subcommand) -> anyhow::Result<()> {
|
||||
match subcommand {
|
||||
Subcommand::Init(init_options) => cli::init(init_options)?,
|
||||
Subcommand::Serve(serve_options) => cli::serve(global, serve_options)?,
|
||||
Subcommand::Build(build_options) => cli::build(build_options)?,
|
||||
Subcommand::Upload(upload_options) => cli::upload(upload_options)?,
|
||||
Subcommand::Doc => cli::doc()?,
|
||||
Subcommand::Plugin(plugin_options) => cli::plugin(plugin_options)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
use librojo::cli::Options;
|
||||
|
||||
fn main() {
|
||||
panic::set_hook(Box::new(|panic_info| {
|
||||
@@ -81,7 +68,7 @@ fn main() {
|
||||
.write_style(options.global.color.into())
|
||||
.init();
|
||||
|
||||
if let Err(err) = run(options.global, options.subcommand) {
|
||||
if let Err(err) = options.run() {
|
||||
log::error!("{:?}", err);
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ use crate::{
|
||||
snapshot_middleware::{snapshot_from_vfs, snapshot_project_node},
|
||||
};
|
||||
|
||||
/// Processes file change events, updates the DOM, and sends those updates
|
||||
/// through a channel for other stuff to consume.
|
||||
///
|
||||
/// Owns the connection between Rojo's VFS and its DOM by holding onto another
|
||||
/// thread that processes messages.
|
||||
///
|
||||
|
||||
146
src/cli/build.rs
146
src/cli/build.rs
@@ -1,24 +1,88 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use fs_err::File;
|
||||
use memofs::Vfs;
|
||||
use thiserror::Error;
|
||||
use structopt::StructOpt;
|
||||
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)]
|
||||
enum OutputKind {
|
||||
/// An XML model file.
|
||||
Rbxmx,
|
||||
|
||||
/// An XML place file.
|
||||
Rbxlx,
|
||||
|
||||
/// A binary model file.
|
||||
Rbxm,
|
||||
|
||||
/// A binary place file.
|
||||
Rbxl,
|
||||
}
|
||||
|
||||
fn detect_output_kind(options: &BuildCommand) -> Option<OutputKind> {
|
||||
let extension = options.output.extension()?.to_str()?;
|
||||
fn detect_output_kind(output: &Path) -> Option<OutputKind> {
|
||||
let extension = output.extension()?.to_str()?;
|
||||
|
||||
match extension {
|
||||
"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 {
|
||||
rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
|
||||
}
|
||||
|
||||
pub fn build(options: BuildCommand) -> Result<(), anyhow::Error> {
|
||||
log::trace!("Constructing in-memory filesystem");
|
||||
|
||||
let vfs = Vfs::new_default();
|
||||
vfs.set_watch_enabled(options.watch);
|
||||
|
||||
let session = ServeSession::new(vfs, &options.absolute_project())?;
|
||||
let mut cursor = session.message_queue().cursor();
|
||||
|
||||
{
|
||||
let tree = session.tree();
|
||||
write_model(&tree, &options)?;
|
||||
}
|
||||
|
||||
if options.watch {
|
||||
let mut rt = Runtime::new().unwrap();
|
||||
|
||||
loop {
|
||||
let receiver = session.message_queue().subscribe(cursor);
|
||||
let (new_cursor, _patch_set) = rt.block_on(receiver).unwrap();
|
||||
cursor = new_cursor;
|
||||
|
||||
let tree = session.tree();
|
||||
write_model(&tree, &options)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), anyhow::Error> {
|
||||
let output_kind = detect_output_kind(&options).ok_or(Error::UnknownOutputKind)?;
|
||||
log::debug!("Hoping to generate file of type {:?}", output_kind);
|
||||
fn write_model(
|
||||
session: &ServeSession,
|
||||
output: &Path,
|
||||
output_kind: OutputKind,
|
||||
) -> anyhow::Result<()> {
|
||||
println!("Building project '{}'", session.project_name());
|
||||
|
||||
let tree = session.tree();
|
||||
let root_id = tree.get_root_id();
|
||||
|
||||
log::trace!("Opening output file for write");
|
||||
let file = File::create(&options.output)?;
|
||||
let mut file = BufWriter::new(file);
|
||||
let mut file = BufWriter::new(File::create(output)?);
|
||||
|
||||
match output_kind {
|
||||
OutputKind::Rbxm => {
|
||||
rbx_binary::to_writer(&mut file, tree.inner(), &[root_id])?;
|
||||
}
|
||||
OutputKind::Rbxl => {
|
||||
let root_instance = tree.get_instance(root_id).unwrap();
|
||||
let top_level_ids = root_instance.children();
|
||||
|
||||
rbx_binary::to_writer(&mut file, tree.inner(), top_level_ids)?;
|
||||
}
|
||||
OutputKind::Rbxmx => {
|
||||
// Model files include the root instance of the tree and all its
|
||||
// descendants.
|
||||
@@ -95,25 +135,15 @@ fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), anyhow::Er
|
||||
|
||||
rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())?;
|
||||
}
|
||||
OutputKind::Rbxm => {
|
||||
rbx_binary::to_writer_default(&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_default(&mut file, tree.inner(), top_level_ids)?;
|
||||
}
|
||||
}
|
||||
|
||||
file.flush()?;
|
||||
|
||||
let filename = options
|
||||
.output
|
||||
let filename = output
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("<invalid utf-8>");
|
||||
log::info!("Built project to {}", filename);
|
||||
println!("Built project to {}", filename);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
pub fn doc() -> Result<(), anyhow::Error> {
|
||||
opener::open("https://rojo.space/docs")?;
|
||||
Ok(())
|
||||
use structopt::StructOpt;
|
||||
|
||||
/// Open Rojo's documentation in your browser.
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct DocCommand {}
|
||||
|
||||
impl DocCommand {
|
||||
pub fn run(self) -> anyhow::Result<()> {
|
||||
opener::open("https://rojo.space/docs")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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::{
|
||||
fs::{self, OpenOptions},
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::str::FromStr;
|
||||
|
||||
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 =
|
||||
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_GIT_IGNORE: &str = include_str!("../../assets/default-place-project/gitignore.txt");
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum Error {
|
||||
#[error("A project file named default.project.json already exists in this folder")]
|
||||
AlreadyExists,
|
||||
/// Initializes a new Rojo project.
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct InitCommand {
|
||||
/// Path to the place to create the project. Defaults to the current directory.
|
||||
#[structopt(default_value = "")]
|
||||
pub path: PathBuf,
|
||||
|
||||
#[error("git init failed")]
|
||||
GitInit,
|
||||
/// The kind of project to create, 'place' or 'model'. Defaults to place.
|
||||
#[structopt(long, default_value = "place")]
|
||||
pub kind: InitKind,
|
||||
}
|
||||
|
||||
pub fn init(options: InitCommand) -> Result<(), anyhow::Error> {
|
||||
let base_path = options.absolute_path();
|
||||
fs::create_dir_all(&base_path)?;
|
||||
impl InitCommand {
|
||||
pub fn run(self) -> anyhow::Result<()> {
|
||||
let base_path = resolve_path(&self.path);
|
||||
fs::create_dir_all(&base_path)?;
|
||||
|
||||
let canonical = fs::canonicalize(&base_path)?;
|
||||
let project_name = canonical
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("new-project");
|
||||
let canonical = fs::canonicalize(&base_path)?;
|
||||
let project_name = canonical
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("new-project");
|
||||
|
||||
let project_params = ProjectParams {
|
||||
name: project_name.to_owned(),
|
||||
};
|
||||
let project_params = ProjectParams {
|
||||
name: project_name.to_owned(),
|
||||
};
|
||||
|
||||
match options.kind {
|
||||
InitKind::Place => init_place(&base_path, project_params),
|
||||
InitKind::Model => init_model(&base_path, project_params),
|
||||
match self.kind {
|
||||
InitKind::Place => init_place(&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> {
|
||||
eprintln!("Creating new place project '{}'", project_params.name);
|
||||
/// The templates we support for initializing a Rojo project.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum InitKind {
|
||||
/// A place that contains a baseplate.
|
||||
Place,
|
||||
|
||||
/// An empty model, suitable for a library or plugin.
|
||||
Model,
|
||||
}
|
||||
|
||||
impl FromStr for InitKind {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(source: &str) -> Result<Self, Self::Err> {
|
||||
match source {
|
||||
"place" => Ok(InitKind::Place),
|
||||
"model" => Ok(InitKind::Model),
|
||||
_ => Err(format_err!(
|
||||
"Invalid init kind '{}'. Valid kinds are: place, model",
|
||||
source
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_place(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> {
|
||||
println!("Creating new place project '{}'", project_params.name);
|
||||
|
||||
let project_file = project_params.render_template(PLACE_PROJECT);
|
||||
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);
|
||||
try_git_init(base_path, &git_ignore)?;
|
||||
|
||||
eprintln!("Created project successfully.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_model(base_path: &Path, project_params: ProjectParams) -> Result<(), anyhow::Error> {
|
||||
eprintln!("Creating new model project '{}'", project_params.name);
|
||||
fn init_model(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> {
|
||||
println!("Creating new model project '{}'", project_params.name);
|
||||
|
||||
let project_file = project_params.render_template(MODEL_PROJECT);
|
||||
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);
|
||||
try_git_init(base_path, &git_ignore)?;
|
||||
|
||||
eprintln!("Created project successfully.");
|
||||
|
||||
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()?;
|
||||
|
||||
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()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(project_path);
|
||||
.open(&project_path);
|
||||
|
||||
let mut file = match file_res {
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
return match err.kind() {
|
||||
io::ErrorKind::AlreadyExists => Err(Error::AlreadyExists.into()),
|
||||
io::ErrorKind::AlreadyExists => {
|
||||
bail!("Project file already exists: {}", project_path.display())
|
||||
}
|
||||
_ => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
243
src/cli/mod.rs
243
src/cli/mod.rs
@@ -2,30 +2,24 @@
|
||||
|
||||
mod build;
|
||||
mod doc;
|
||||
mod fmt_project;
|
||||
mod init;
|
||||
mod plugin;
|
||||
mod serve;
|
||||
mod upload;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
env,
|
||||
error::Error,
|
||||
fmt,
|
||||
net::IpAddr,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
use std::{borrow::Cow, env, path::Path, str::FromStr};
|
||||
|
||||
use structopt::StructOpt;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use self::build::*;
|
||||
pub use self::doc::*;
|
||||
pub use self::init::*;
|
||||
pub use self::plugin::*;
|
||||
pub use self::serve::*;
|
||||
pub use self::upload::*;
|
||||
pub use self::build::BuildCommand;
|
||||
pub use self::doc::DocCommand;
|
||||
pub use self::fmt_project::FmtProjectCommand;
|
||||
pub use self::init::{InitCommand, InitKind};
|
||||
pub use self::plugin::{PluginCommand, PluginSubcommand};
|
||||
pub use self::serve::ServeCommand;
|
||||
pub use self::upload::UploadCommand;
|
||||
|
||||
/// Command line options that Rojo accepts, defined using the structopt crate.
|
||||
#[derive(Debug, StructOpt)]
|
||||
@@ -39,6 +33,20 @@ pub struct Options {
|
||||
pub subcommand: Subcommand,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub fn run(self) -> anyhow::Result<()> {
|
||||
match self.subcommand {
|
||||
Subcommand::Init(subcommand) => subcommand.run(),
|
||||
Subcommand::Serve(subcommand) => subcommand.run(self.global),
|
||||
Subcommand::Build(subcommand) => subcommand.run(),
|
||||
Subcommand::Upload(subcommand) => subcommand.run(),
|
||||
Subcommand::FmtProject(subcommand) => subcommand.run(),
|
||||
Subcommand::Doc(subcommand) => subcommand.run(),
|
||||
Subcommand::Plugin(subcommand) => subcommand.run(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct GlobalOptions {
|
||||
/// Sets verbosity level. Can be specified multiple times.
|
||||
@@ -100,218 +108,19 @@ pub struct ColorChoiceParseError {
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum Subcommand {
|
||||
/// Creates a new Rojo project.
|
||||
Init(InitCommand),
|
||||
|
||||
/// Serves the project's files for use with the Rojo Studio plugin.
|
||||
Serve(ServeCommand),
|
||||
|
||||
/// Generates a model or place file from the project.
|
||||
Build(BuildCommand),
|
||||
|
||||
/// Generates a place or model file out of the project and uploads it to Roblox.
|
||||
Upload(UploadCommand),
|
||||
|
||||
/// Open Rojo's documentation in your browser.
|
||||
Doc,
|
||||
|
||||
/// Manages Rojo's Roblox Studio plugin.
|
||||
FmtProject(FmtProjectCommand),
|
||||
Doc(DocCommand),
|
||||
Plugin(PluginCommand),
|
||||
}
|
||||
|
||||
/// Initializes a new Rojo project.
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct InitCommand {
|
||||
/// Path to the place to create the project. Defaults to the current directory.
|
||||
#[structopt(default_value = "")]
|
||||
pub path: PathBuf,
|
||||
|
||||
/// The kind of project to create, 'place' or 'model'. Defaults to place.
|
||||
#[structopt(long, default_value = "place")]
|
||||
pub kind: InitKind,
|
||||
}
|
||||
|
||||
impl InitCommand {
|
||||
pub fn absolute_path(&self) -> Cow<'_, Path> {
|
||||
resolve_path(&self.path)
|
||||
}
|
||||
}
|
||||
|
||||
/// The templates we support for initializing a Rojo project.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum InitKind {
|
||||
/// A place that matches what File -> New does in Roblox Studio.
|
||||
Place,
|
||||
|
||||
/// An empty model, suitable for a library or plugin.
|
||||
Model,
|
||||
}
|
||||
|
||||
impl FromStr for InitKind {
|
||||
type Err = InitKindParseError;
|
||||
|
||||
fn from_str(source: &str) -> Result<Self, Self::Err> {
|
||||
match source {
|
||||
"place" => Ok(InitKind::Place),
|
||||
"model" => Ok(InitKind::Model),
|
||||
_ => Err(InitKindParseError {
|
||||
attempted: source.to_owned(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for failing to parse an `InitKind`.
|
||||
#[derive(Debug)]
|
||||
pub struct InitKindParseError {
|
||||
attempted: String,
|
||||
}
|
||||
|
||||
impl Error for InitKindParseError {}
|
||||
|
||||
impl fmt::Display for InitKindParseError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
formatter,
|
||||
"Invalid init kind '{}'. Valid kinds are: place, model",
|
||||
self.attempted
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Expose a Rojo project through a web server that can communicate with the
|
||||
/// Rojo Roblox Studio plugin, or be visited by the user in the browser.
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct ServeCommand {
|
||||
/// Path to the project to serve. Defaults to the current directory.
|
||||
#[structopt(default_value = "")]
|
||||
pub project: PathBuf,
|
||||
|
||||
/// The IP address to listen on. Defaults to `127.0.0.1`.
|
||||
#[structopt(long)]
|
||||
pub address: Option<IpAddr>,
|
||||
|
||||
/// The port to listen on. Defaults to the project's preference, or `34872` if
|
||||
/// it has none.
|
||||
#[structopt(long)]
|
||||
pub port: Option<u16>,
|
||||
}
|
||||
|
||||
impl ServeCommand {
|
||||
pub fn absolute_project(&self) -> Cow<'_, Path> {
|
||||
resolve_path(&self.project)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a Rojo project into a file.
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct BuildCommand {
|
||||
/// Path to the project to serve. Defaults to the current directory.
|
||||
#[structopt(default_value = "")]
|
||||
pub project: PathBuf,
|
||||
|
||||
/// Where to output the result.
|
||||
#[structopt(long, short)]
|
||||
pub output: PathBuf,
|
||||
|
||||
/// Whether to automatically rebuild when any input files change.
|
||||
#[structopt(long)]
|
||||
pub watch: bool,
|
||||
}
|
||||
|
||||
impl BuildCommand {
|
||||
pub fn absolute_project(&self) -> Cow<'_, Path> {
|
||||
resolve_path(&self.project)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build and upload a Rojo project to Roblox.com.
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct UploadCommand {
|
||||
/// Path to the project to upload. Defaults to the current directory.
|
||||
#[structopt(default_value = "")]
|
||||
pub project: PathBuf,
|
||||
|
||||
/// Authenication cookie to use. If not specified, Rojo will attempt to find one from the system automatically.
|
||||
#[structopt(long)]
|
||||
pub cookie: Option<String>,
|
||||
|
||||
/// Asset ID to upload to.
|
||||
#[structopt(long = "asset_id")]
|
||||
pub asset_id: u64,
|
||||
}
|
||||
|
||||
impl UploadCommand {
|
||||
pub fn absolute_project(&self) -> Cow<'_, Path> {
|
||||
resolve_path(&self.project)
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of asset to upload to the website. Affects what endpoints Rojo uses
|
||||
/// and changes how the asset is built.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum UploadKind {
|
||||
/// Upload to a place.
|
||||
Place,
|
||||
|
||||
/// Upload to a model-like asset, like a Model, Plugin, or Package.
|
||||
Model,
|
||||
}
|
||||
|
||||
impl FromStr for UploadKind {
|
||||
type Err = UploadKindParseError;
|
||||
|
||||
fn from_str(source: &str) -> Result<Self, Self::Err> {
|
||||
match source {
|
||||
"place" => Ok(UploadKind::Place),
|
||||
"model" => Ok(UploadKind::Model),
|
||||
_ => Err(UploadKindParseError {
|
||||
attempted: source.to_owned(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for failing to parse an `UploadKind`.
|
||||
#[derive(Debug)]
|
||||
pub struct UploadKindParseError {
|
||||
attempted: String,
|
||||
}
|
||||
|
||||
impl Error for UploadKindParseError {}
|
||||
|
||||
impl fmt::Display for UploadKindParseError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
formatter,
|
||||
"Invalid upload kind '{}'. Valid kinds are: place, model",
|
||||
self.attempted
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_path(path: &Path) -> Cow<'_, Path> {
|
||||
pub(super) fn resolve_path(path: &Path) -> Cow<'_, Path> {
|
||||
if path.is_absolute() {
|
||||
Cow::Borrowed(path)
|
||||
} else {
|
||||
Cow::Owned(env::current_dir().unwrap().join(path))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum PluginSubcommand {
|
||||
/// Install the plugin in Roblox Studio's plugins folder. If the plugin is
|
||||
/// already installed, installing it again will overwrite the current plugin
|
||||
/// file.
|
||||
Install,
|
||||
|
||||
/// Removes the plugin if it is installed.
|
||||
Uninstall,
|
||||
}
|
||||
|
||||
/// Install Rojo's plugin.
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct PluginCommand {
|
||||
#[structopt(subcommand)]
|
||||
subcommand: PluginSubcommand,
|
||||
}
|
||||
|
||||
@@ -3,26 +3,50 @@ use std::{
|
||||
io::BufWriter,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use memofs::{InMemoryFs, Vfs, VfsSnapshot};
|
||||
use roblox_install::RobloxStudio;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use crate::{
|
||||
cli::{PluginCommand, PluginSubcommand},
|
||||
serve_session::ServeSession,
|
||||
};
|
||||
use crate::serve_session::ServeSession;
|
||||
|
||||
static PLUGIN_BINCODE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/plugin.bincode"));
|
||||
static PLUGIN_FILE_NAME: &str = "RojoManagedPlugin.rbxm";
|
||||
|
||||
pub fn plugin(options: PluginCommand) -> Result<()> {
|
||||
match options.subcommand {
|
||||
PluginSubcommand::Install => install_plugin(),
|
||||
PluginSubcommand::Uninstall => uninstall_plugin(),
|
||||
/// Install Rojo's plugin.
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct PluginCommand {
|
||||
#[structopt(subcommand)]
|
||||
subcommand: PluginSubcommand,
|
||||
}
|
||||
|
||||
/// Manages Rojo's Roblox Studio plugin.
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum PluginSubcommand {
|
||||
/// Install the plugin in Roblox Studio's plugins folder. If the plugin is
|
||||
/// already installed, installing it again will overwrite the current plugin
|
||||
/// file.
|
||||
Install,
|
||||
|
||||
/// Removes the plugin if it is installed.
|
||||
Uninstall,
|
||||
}
|
||||
|
||||
impl PluginCommand {
|
||||
pub fn run(self) -> anyhow::Result<()> {
|
||||
self.subcommand.run()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn 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)
|
||||
.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 root_id = tree.get_root_id();
|
||||
|
||||
rbx_binary::to_writer_default(&mut file, tree.inner(), &[root_id])?;
|
||||
rbx_binary::to_writer(&mut file, tree.inner(), &[root_id])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall_plugin() -> Result<()> {
|
||||
fn uninstall_plugin() -> anyhow::Result<()> {
|
||||
let studio = RobloxStudio::locate()?;
|
||||
|
||||
let plugin_path = studio.plugins_path().join(PLUGIN_FILE_NAME);
|
||||
|
||||
@@ -1,52 +1,73 @@
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
net::IpAddr,
|
||||
net::Ipv4Addr,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use memofs::Vfs;
|
||||
use structopt::StructOpt;
|
||||
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
|
||||
|
||||
use crate::{
|
||||
cli::{GlobalOptions, ServeCommand},
|
||||
serve_session::ServeSession,
|
||||
web::LiveServer,
|
||||
};
|
||||
use crate::{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;
|
||||
|
||||
pub fn serve(global: GlobalOptions, options: ServeCommand) -> Result<()> {
|
||||
let vfs = Vfs::new_default();
|
||||
/// Expose a Rojo project to the Rojo Studio plugin.
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct ServeCommand {
|
||||
/// Path to the project to serve. Defaults to the current directory.
|
||||
#[structopt(default_value = "")]
|
||||
pub project: PathBuf,
|
||||
|
||||
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 ip = options.address.unwrap_or(DEFAULT_BIND_ADDRESS.into());
|
||||
/// The port to listen on. Defaults to the project's preference, or `34872` if
|
||||
/// it has none.
|
||||
#[structopt(long)]
|
||||
pub port: Option<u16>,
|
||||
}
|
||||
|
||||
let port = options
|
||||
.port
|
||||
.or_else(|| session.project_port())
|
||||
.unwrap_or(DEFAULT_PORT);
|
||||
impl ServeCommand {
|
||||
pub fn run(self, global: GlobalOptions) -> anyhow::Result<()> {
|
||||
let project_path = resolve_path(&self.project);
|
||||
|
||||
let server = LiveServer::new(session);
|
||||
let vfs = Vfs::new_default();
|
||||
|
||||
let _ = show_start_message(ip, port, global.color.into());
|
||||
server.start((ip, port).into());
|
||||
let session = Arc::new(ServeSession::new(vfs, &project_path)?);
|
||||
|
||||
Ok(())
|
||||
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 mut buffer = writer.buffer();
|
||||
|
||||
writeln!(&mut buffer, "Rojo server listening:")?;
|
||||
|
||||
write!(&mut buffer, " Address: ")?;
|
||||
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
|
||||
|
||||
buffer.set_color(&green)?;
|
||||
if bind_address.is_loopback() {
|
||||
writeln!(&mut buffer, "localhost")?;
|
||||
} else {
|
||||
@@ -55,7 +76,7 @@ fn show_start_message(bind_address: IpAddr, port: u16, color: ColorChoice) -> io
|
||||
|
||||
buffer.set_color(&ColorSpec::new())?;
|
||||
write!(&mut buffer, " Port: ")?;
|
||||
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
|
||||
buffer.set_color(&green)?;
|
||||
writeln!(&mut buffer, "{}", port)?;
|
||||
|
||||
writeln!(&mut buffer)?;
|
||||
@@ -63,7 +84,7 @@ fn show_start_message(bind_address: IpAddr, port: u16, color: ColorChoice) -> io
|
||||
buffer.set_color(&ColorSpec::new())?;
|
||||
write!(&mut buffer, "Visit ")?;
|
||||
|
||||
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
|
||||
buffer.set_color(&green)?;
|
||||
write!(&mut buffer, "http://localhost:{}/", port)?;
|
||||
|
||||
buffer.set_color(&ColorSpec::new())?;
|
||||
|
||||
@@ -1,46 +1,87 @@
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, format_err, Context};
|
||||
use memofs::Vfs;
|
||||
use reqwest::{
|
||||
header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT},
|
||||
StatusCode,
|
||||
};
|
||||
use thiserror::Error;
|
||||
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)]
|
||||
enum Error {
|
||||
#[error("Rojo could not find your Roblox auth cookie. Please pass one via --cookie.")]
|
||||
NeedAuthCookie,
|
||||
use super::resolve_path;
|
||||
|
||||
#[error("The Roblox API returned an unexpected error: {body}")]
|
||||
RobloxApi { body: String },
|
||||
/// Builds the project and uploads it to Roblox.
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct UploadCommand {
|
||||
/// Path to the project to upload. Defaults to the current directory.
|
||||
#[structopt(default_value = "")]
|
||||
pub project: PathBuf,
|
||||
|
||||
/// Authenication cookie to use. If not specified, Rojo will attempt to find one from the system automatically.
|
||||
#[structopt(long)]
|
||||
pub cookie: Option<String>,
|
||||
|
||||
/// Asset ID to upload to.
|
||||
#[structopt(long = "asset_id")]
|
||||
pub asset_id: u64,
|
||||
}
|
||||
|
||||
pub fn upload(options: UploadCommand) -> Result<(), anyhow::Error> {
|
||||
let cookie = options
|
||||
.cookie
|
||||
.clone()
|
||||
.or_else(get_auth_cookie)
|
||||
.ok_or(Error::NeedAuthCookie)?;
|
||||
impl UploadCommand {
|
||||
pub fn run(self) -> Result<(), anyhow::Error> {
|
||||
let project_path = resolve_path(&self.project);
|
||||
|
||||
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 inner_tree = tree.inner();
|
||||
let root = inner_tree.root();
|
||||
let session = ServeSession::new(vfs, project_path)?;
|
||||
|
||||
let encode_ids = match root.class.as_str() {
|
||||
"DataModel" => root.children().to_vec(),
|
||||
_ => vec![root.referent()],
|
||||
};
|
||||
let tree = session.tree();
|
||||
let inner_tree = tree.inner();
|
||||
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 binary model");
|
||||
rbx_binary::to_writer_default(&mut buffer, tree.inner(), &encode_ids)?;
|
||||
do_upload(buffer, options.asset_id, &cookie)
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
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<()> {
|
||||
@@ -76,10 +117,10 @@ fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()>
|
||||
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(Error::RobloxApi {
|
||||
body: response.text()?,
|
||||
}
|
||||
.into());
|
||||
bail!(
|
||||
"The Roblox API returned an unexpected error: {}",
|
||||
response.text()?
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -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::{
|
||||
fmt::{self, Write},
|
||||
|
||||
@@ -1,26 +1,6 @@
|
||||
use std::sync::{Mutex, RwLock};
|
||||
|
||||
use futures::sync::oneshot;
|
||||
|
||||
struct Listener<T> {
|
||||
sender: oneshot::Sender<(u32, Vec<T>)>,
|
||||
cursor: u32,
|
||||
}
|
||||
|
||||
fn fire_listener_if_ready<T: Clone>(
|
||||
messages: &[T],
|
||||
listener: Listener<T>,
|
||||
) -> Result<(), Listener<T>> {
|
||||
let current_cursor = messages.len() as u32;
|
||||
|
||||
if listener.cursor < current_cursor {
|
||||
let new_messages = messages[(listener.cursor as usize)..].to_vec();
|
||||
let _ = listener.sender.send((current_cursor, new_messages));
|
||||
Ok(())
|
||||
} else {
|
||||
Err(listener)
|
||||
}
|
||||
}
|
||||
use futures::channel::oneshot;
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,10 +59,12 @@ pub struct Project {
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
use std::borrow::Borrow;
|
||||
|
||||
use anyhow::format_err;
|
||||
use rbx_dom_weak::types::{
|
||||
Color3, Color3uint8, Content, Enum, Variant, VariantType, Vector2, Vector3,
|
||||
};
|
||||
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 {
|
||||
@@ -46,13 +52,14 @@ impl AmbiguousValue {
|
||||
})?;
|
||||
|
||||
let error = |what: &str| {
|
||||
let sample_values = enum_descriptor
|
||||
let mut all_values = enum_descriptor
|
||||
.items
|
||||
.keys()
|
||||
.take(3)
|
||||
.map(|name| format!(r#""{}""#, name))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
.map(|value| value.borrow())
|
||||
.collect::<Vec<_>>();
|
||||
all_values.sort();
|
||||
|
||||
let examples = nonexhaustive_list(&all_values);
|
||||
|
||||
format_err!(
|
||||
"Invalid value for property {}.{}. Got {} but \
|
||||
@@ -61,7 +68,7 @@ impl AmbiguousValue {
|
||||
prop_name,
|
||||
what,
|
||||
enum_name,
|
||||
sample_values
|
||||
examples,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -101,15 +108,6 @@ impl AmbiguousValue {
|
||||
(VariantType::Color3, AmbiguousValue::Array3(value)) => {
|
||||
Ok(Color3::new(value[0] as f32, value[1] as f32, value[2] as f32).into())
|
||||
}
|
||||
(VariantType::Color3uint8, AmbiguousValue::Array3(value)) => {
|
||||
let value = Color3uint8::new(
|
||||
(value[0] / 255.0) as u8,
|
||||
(value[1] / 255.0) as u8,
|
||||
(value[2] / 255.0) as u8,
|
||||
);
|
||||
|
||||
Ok(value.into())
|
||||
}
|
||||
|
||||
(_, unresolved) => Err(format_err!(
|
||||
"Wrong type of value for property {}.{}. Expected {:?}, got {}",
|
||||
@@ -155,3 +153,121 @@ fn find_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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ use crate::{
|
||||
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
|
||||
/// interact with a serve session is Rojo's HTTP right now, there's no reason
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot/tests/apply.rs
|
||||
expression: applied_patch_value
|
||||
|
||||
---
|
||||
removed: []
|
||||
added: []
|
||||
@@ -10,6 +11,6 @@ updated:
|
||||
changed_class_name: ~
|
||||
changed_properties:
|
||||
Foo:
|
||||
Type: String
|
||||
Value: Value of Foo
|
||||
String: Value of Foo
|
||||
changed_metadata: ~
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
---
|
||||
source: src/snapshot/tests/apply.rs
|
||||
expression: tree_view
|
||||
|
||||
---
|
||||
id: id-1
|
||||
name: ROOT
|
||||
class_name: ROOT
|
||||
properties:
|
||||
Foo:
|
||||
Type: String
|
||||
Value: Value of Foo
|
||||
String: Value of Foo
|
||||
metadata:
|
||||
ignore_unknown_instances: false
|
||||
relevant_paths: []
|
||||
context: {}
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
---
|
||||
source: src/snapshot/tests/apply.rs
|
||||
expression: tree_view
|
||||
|
||||
---
|
||||
id: id-1
|
||||
name: ROOT
|
||||
class_name: ROOT
|
||||
properties:
|
||||
Foo:
|
||||
Type: String
|
||||
Value: Should be removed
|
||||
String: Should be removed
|
||||
metadata:
|
||||
ignore_unknown_instances: false
|
||||
relevant_paths: []
|
||||
context: {}
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot/tests/compute.rs
|
||||
expression: patch_value
|
||||
|
||||
---
|
||||
removed_instances: []
|
||||
added_instances: []
|
||||
@@ -10,6 +11,6 @@ updated_instances:
|
||||
changed_class_name: ~
|
||||
changed_properties:
|
||||
PropertyName:
|
||||
Type: String
|
||||
Value: "Hello, world!"
|
||||
String: "Hello, world!"
|
||||
changed_metadata: ~
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Defines the semantics that Rojo uses to turn entries on the filesystem into
|
||||
//! Roblox instances using the instance snapshot subsystem.
|
||||
//!
|
||||
//! These modules define how files turn into instances.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
@@ -38,6 +40,8 @@ use self::{
|
||||
|
||||
pub use self::project::snapshot_project_node;
|
||||
|
||||
/// The main entrypoint to the snapshot function. This function can be pointed
|
||||
/// at any path and will return something if Rojo knows how to deal with it.
|
||||
pub fn snapshot_from_vfs(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
|
||||
@@ -360,8 +360,7 @@ mod test {
|
||||
"$className": "StringValue",
|
||||
"$properties": {
|
||||
"Value": {
|
||||
"Type": "String",
|
||||
"Value": "Hello, world!"
|
||||
"String": "Hello, world!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ pub fn snapshot_rbxm(
|
||||
path: &Path,
|
||||
instance_name: &str,
|
||||
) -> SnapshotInstanceResult {
|
||||
let temp_tree = rbx_binary::from_reader_default(vfs.read(path)?.as_slice())
|
||||
let temp_tree = rbx_binary::from_reader(vfs.read(path)?.as_slice())
|
||||
.with_context(|| format!("Malformed rbxm file: {}", path.display()))?;
|
||||
|
||||
let root_instance = temp_tree.root();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/csv.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -15,6 +16,6 @@ name: foo
|
||||
class_name: LocalizationTable
|
||||
properties:
|
||||
Contents:
|
||||
Type: String
|
||||
Value: "[{\"key\":\"Ack\",\"example\":\"An exclamation of despair\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]"
|
||||
String: "[{\"key\":\"Ack\",\"example\":\"An exclamation of despair\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]"
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/csv.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -15,6 +16,6 @@ name: foo
|
||||
class_name: LocalizationTable
|
||||
properties:
|
||||
Contents:
|
||||
Type: String
|
||||
Value: "[{\"key\":\"Ack\",\"example\":\"An exclamation of despair\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]"
|
||||
String: "[{\"key\":\"Ack\",\"example\":\"An exclamation of despair\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]"
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/json.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -15,6 +16,6 @@ name: foo
|
||||
class_name: ModuleScript
|
||||
properties:
|
||||
Source:
|
||||
Type: String
|
||||
Value: "return {\n\t[\"1invalidident\"] = \"nice\",\n\tarray = {1, 2, 3},\n\t[\"false\"] = false,\n\tfloat = 1234.5452,\n\tint = 1234,\n\tnull = nil,\n\tobject = {\n\t\thello = \"world\",\n\t},\n\t[\"true\"] = true,\n}"
|
||||
String: "return {\n\t[\"1invalidident\"] = \"nice\",\n\tarray = {1, 2, 3},\n\t[\"false\"] = false,\n\tfloat = 1234.5452,\n\tint = 1234,\n\tnull = nil,\n\tobject = {\n\t\thello = \"world\",\n\t},\n\t[\"true\"] = true,\n}"
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/json_model.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -14,8 +15,7 @@ name: foo
|
||||
class_name: IntValue
|
||||
properties:
|
||||
Value:
|
||||
Type: Int64
|
||||
Value: 5
|
||||
Int64: 5
|
||||
children:
|
||||
- snapshot_id: ~
|
||||
metadata:
|
||||
@@ -26,3 +26,4 @@ children:
|
||||
class_name: StringValue
|
||||
properties: {}
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/lua.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -15,6 +16,6 @@ name: foo
|
||||
class_name: LocalScript
|
||||
properties:
|
||||
Source:
|
||||
Type: String
|
||||
Value: Hello there!
|
||||
String: Hello there!
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/lua.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -15,6 +16,6 @@ name: foo
|
||||
class_name: ModuleScript
|
||||
properties:
|
||||
Source:
|
||||
Type: String
|
||||
Value: Hello there!
|
||||
String: Hello there!
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/lua.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -15,6 +16,6 @@ name: foo
|
||||
class_name: ModuleScript
|
||||
properties:
|
||||
Source:
|
||||
Type: String
|
||||
Value: Hello there!
|
||||
String: Hello there!
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/lua.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -15,9 +16,8 @@ name: bar
|
||||
class_name: Script
|
||||
properties:
|
||||
Disabled:
|
||||
Type: Bool
|
||||
Value: true
|
||||
Bool: true
|
||||
Source:
|
||||
Type: String
|
||||
Value: Hello there!
|
||||
String: Hello there!
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/lua.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -15,6 +16,6 @@ name: foo
|
||||
class_name: Script
|
||||
properties:
|
||||
Source:
|
||||
Type: String
|
||||
Value: Hello there!
|
||||
String: Hello there!
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/lua.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -15,6 +16,6 @@ name: foo
|
||||
class_name: Script
|
||||
properties:
|
||||
Source:
|
||||
Type: String
|
||||
Value: Hello there!
|
||||
String: Hello there!
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/project.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -15,6 +16,6 @@ name: path-property-override
|
||||
class_name: StringValue
|
||||
properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: Changed
|
||||
String: Changed
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/project.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -16,6 +17,6 @@ name: path-project
|
||||
class_name: StringValue
|
||||
properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "Hello, world!"
|
||||
String: "Hello, world!"
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/project.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -14,6 +15,6 @@ name: resolved-properties
|
||||
class_name: StringValue
|
||||
properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: "Hello, world!"
|
||||
String: "Hello, world!"
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/project.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -14,6 +15,6 @@ name: unresolved-properties
|
||||
class_name: StringValue
|
||||
properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: Hi!
|
||||
String: Hi!
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
source: src/snapshot_middleware/txt.rs
|
||||
expression: instance_snapshot
|
||||
|
||||
---
|
||||
snapshot_id: ~
|
||||
metadata:
|
||||
@@ -15,6 +16,6 @@ name: foo
|
||||
class_name: StringValue
|
||||
properties:
|
||||
Value:
|
||||
Type: String
|
||||
Value: Hello there!
|
||||
String: Hello there!
|
||||
children: []
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! Utiilty that helps redact nondeterministic information from trees so that
|
||||
//! they can be part of snapshot tests.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rbx_dom_weak::types::{Ref, Variant};
|
||||
|
||||
140
src/web/api.rs
140
src/web/api.rs
@@ -3,9 +3,7 @@
|
||||
|
||||
use std::{collections::HashMap, fs, path::PathBuf, str::FromStr, sync::Arc};
|
||||
|
||||
use futures::{Future, Stream};
|
||||
|
||||
use hyper::{service::Service, Body, Method, Request, StatusCode};
|
||||
use hyper::{body, Body, Method, Request, Response, StatusCode};
|
||||
use rbx_dom_weak::types::Ref;
|
||||
|
||||
use crate::{
|
||||
@@ -21,36 +19,32 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
pub struct ApiService {
|
||||
serve_session: Arc<ServeSession>,
|
||||
pub async fn call(serve_session: Arc<ServeSession>, request: Request<Body>) -> Response<Body> {
|
||||
let service = ApiService::new(serve_session);
|
||||
|
||||
match (request.method(), request.uri().path()) {
|
||||
(&Method::GET, "/api/rojo") => service.handle_api_rojo().await,
|
||||
(&Method::GET, path) if path.starts_with("/api/read/") => {
|
||||
service.handle_api_read(request).await
|
||||
}
|
||||
(&Method::GET, path) if path.starts_with("/api/subscribe/") => {
|
||||
service.handle_api_subscribe(request).await
|
||||
}
|
||||
(&Method::POST, path) if path.starts_with("/api/open/") => {
|
||||
service.handle_api_open(request).await
|
||||
}
|
||||
|
||||
(&Method::POST, "/api/write") => service.handle_api_write(request).await,
|
||||
|
||||
(_method, path) => json(
|
||||
ErrorResponse::not_found(format!("Route not found: {}", path)),
|
||||
StatusCode::NOT_FOUND,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
impl Service for ApiService {
|
||||
type ReqBody = Body;
|
||||
type ResBody = Body;
|
||||
type Error = hyper::Error;
|
||||
type Future =
|
||||
Box<dyn Future<Item = hyper::Response<Self::ReqBody>, Error = Self::Error> + Send>;
|
||||
|
||||
fn call(&mut self, request: hyper::Request<Self::ReqBody>) -> Self::Future {
|
||||
match (request.method(), request.uri().path()) {
|
||||
(&Method::GET, "/api/rojo") => self.handle_api_rojo(),
|
||||
(&Method::GET, path) if path.starts_with("/api/read/") => self.handle_api_read(request),
|
||||
(&Method::GET, path) if path.starts_with("/api/subscribe/") => {
|
||||
self.handle_api_subscribe(request)
|
||||
}
|
||||
(&Method::POST, path) if path.starts_with("/api/open/") => {
|
||||
self.handle_api_open(request)
|
||||
}
|
||||
|
||||
(&Method::POST, "/api/write") => self.handle_api_write(request),
|
||||
|
||||
(_method, path) => json(
|
||||
ErrorResponse::not_found(format!("Route not found: {}", path)),
|
||||
StatusCode::NOT_FOUND,
|
||||
),
|
||||
}
|
||||
}
|
||||
pub struct ApiService {
|
||||
serve_session: Arc<ServeSession>,
|
||||
}
|
||||
|
||||
impl ApiService {
|
||||
@@ -59,7 +53,7 @@ impl ApiService {
|
||||
}
|
||||
|
||||
/// Get a summary of information about the server
|
||||
fn handle_api_rojo(&self) -> <Self as Service>::Future {
|
||||
async fn handle_api_rojo(&self) -> Response<Body> {
|
||||
let tree = self.serve_session.tree();
|
||||
let root_instance_id = tree.get_root_id();
|
||||
|
||||
@@ -77,7 +71,7 @@ impl ApiService {
|
||||
|
||||
/// Retrieve any messages past the given cursor index, and if
|
||||
/// there weren't any, subscribe to receive any new messages.
|
||||
fn handle_api_subscribe(&self, request: Request<Body>) -> <Self as Service>::Future {
|
||||
async fn handle_api_subscribe(&self, request: Request<Body>) -> Response<Body> {
|
||||
let argument = &request.uri().path()["/api/subscribe/".len()..];
|
||||
let input_cursor: u32 = match argument.parse() {
|
||||
Ok(v) => v,
|
||||
@@ -91,11 +85,15 @@ impl ApiService {
|
||||
|
||||
let session_id = self.serve_session.session_id();
|
||||
|
||||
let receiver = self.serve_session.message_queue().subscribe(input_cursor);
|
||||
let result = self
|
||||
.serve_session
|
||||
.message_queue()
|
||||
.subscribe(input_cursor)
|
||||
.await;
|
||||
|
||||
let tree_handle = self.serve_session.tree_handle();
|
||||
|
||||
Box::new(receiver.then(move |result| match result {
|
||||
match result {
|
||||
Ok((message_cursor, messages)) => {
|
||||
let tree = tree_handle.lock().unwrap();
|
||||
|
||||
@@ -151,56 +149,56 @@ impl ApiService {
|
||||
ErrorResponse::internal_error("Message queue disconnected sender"),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_api_write(&self, request: Request<Body>) -> <Self as Service>::Future {
|
||||
async fn handle_api_write(&self, request: Request<Body>) -> Response<Body> {
|
||||
let session_id = self.serve_session.session_id();
|
||||
let tree_mutation_sender = self.serve_session.tree_mutation_sender();
|
||||
|
||||
Box::new(request.into_body().concat2().and_then(move |body| {
|
||||
let request: WriteRequest = match serde_json::from_slice(&body) {
|
||||
Ok(request) => request,
|
||||
Err(err) => {
|
||||
return json(
|
||||
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
||||
StatusCode::BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
};
|
||||
let body = body::to_bytes(request.into_body()).await.unwrap();
|
||||
|
||||
if request.session_id != session_id {
|
||||
let request: WriteRequest = match serde_json::from_slice(&body) {
|
||||
Ok(request) => request,
|
||||
Err(err) => {
|
||||
return json(
|
||||
ErrorResponse::bad_request("Wrong session ID"),
|
||||
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
||||
StatusCode::BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let updated_instances = request
|
||||
.updated
|
||||
.into_iter()
|
||||
.map(|update| PatchUpdate {
|
||||
id: update.id,
|
||||
changed_class_name: update.changed_class_name,
|
||||
changed_name: update.changed_name,
|
||||
changed_properties: update.changed_properties,
|
||||
changed_metadata: None,
|
||||
})
|
||||
.collect();
|
||||
if request.session_id != session_id {
|
||||
return json(
|
||||
ErrorResponse::bad_request("Wrong session ID"),
|
||||
StatusCode::BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
tree_mutation_sender
|
||||
.send(PatchSet {
|
||||
removed_instances: Vec::new(),
|
||||
added_instances: Vec::new(),
|
||||
updated_instances,
|
||||
})
|
||||
.unwrap();
|
||||
let updated_instances = request
|
||||
.updated
|
||||
.into_iter()
|
||||
.map(|update| PatchUpdate {
|
||||
id: update.id,
|
||||
changed_class_name: update.changed_class_name,
|
||||
changed_name: update.changed_name,
|
||||
changed_properties: update.changed_properties,
|
||||
changed_metadata: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
json_ok(&WriteResponse { session_id })
|
||||
}))
|
||||
tree_mutation_sender
|
||||
.send(PatchSet {
|
||||
removed_instances: Vec::new(),
|
||||
added_instances: Vec::new(),
|
||||
updated_instances,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
json_ok(&WriteResponse { session_id })
|
||||
}
|
||||
|
||||
fn handle_api_read(&self, request: Request<Body>) -> <Self as Service>::Future {
|
||||
async fn handle_api_read(&self, request: Request<Body>) -> Response<Body> {
|
||||
let argument = &request.uri().path()["/api/read/".len()..];
|
||||
let requested_ids: Result<Vec<Ref>, _> = argument.split(',').map(Ref::from_str).collect();
|
||||
|
||||
@@ -239,7 +237,7 @@ impl ApiService {
|
||||
}
|
||||
|
||||
/// Open a script with the given ID in the user's default text editor.
|
||||
fn handle_api_open(&self, request: Request<Body>) -> <Self as Service>::Future {
|
||||
async fn handle_api_open(&self, request: Request<Body>) -> Response<Body> {
|
||||
let argument = &request.uri().path()["/api/open/".len()..];
|
||||
let requested_id = match Ref::from_str(argument) {
|
||||
Ok(id) => id,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! A simple asset reloading system intended for iterating on CSS. When in the
|
||||
//! `dev_live_assets` feature is enabled, files are read from disk and watched
|
||||
//! for changes. Otherwise, they're baked into the executable.
|
||||
|
||||
macro_rules! declare_asset {
|
||||
($name: ident, $path: expr) => {
|
||||
pub fn $name() -> &'static str {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
//! Defines all the structs needed to interact with the Rojo Serve API.
|
||||
//! Defines all the structs needed to interact with the Rojo Serve API. This is
|
||||
//! useful for tests to be able to use the same data structures as the
|
||||
//! implementation.
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
|
||||
@@ -1,53 +1,26 @@
|
||||
//! Defines the Rojo web interface. This is what the Roblox Studio plugin
|
||||
//! communicates with. Eventually, we'll make this API stable, produce better
|
||||
//! documentation for it, and open it up for other consumers.
|
||||
|
||||
mod api;
|
||||
mod assets;
|
||||
pub mod interface;
|
||||
mod ui;
|
||||
mod util;
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
use std::convert::Infallible;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::{
|
||||
future::{self, FutureResult},
|
||||
Future,
|
||||
use hyper::{
|
||||
server::Server,
|
||||
service::{make_service_fn, service_fn},
|
||||
Body, Request,
|
||||
};
|
||||
use hyper::{service::Service, Body, Request, Response, Server};
|
||||
use log::trace;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::serve_session::ServeSession;
|
||||
|
||||
use self::{api::ApiService, ui::UiService};
|
||||
|
||||
pub struct RootService {
|
||||
api: ApiService,
|
||||
ui: UiService,
|
||||
}
|
||||
|
||||
impl Service for RootService {
|
||||
type ReqBody = Body;
|
||||
type ResBody = Body;
|
||||
type Error = hyper::Error;
|
||||
type Future = Box<dyn Future<Item = Response<Self::ReqBody>, Error = Self::Error> + Send>;
|
||||
|
||||
fn call(&mut self, request: Request<Self::ReqBody>) -> Self::Future {
|
||||
trace!("{} {}", request.method(), request.uri().path());
|
||||
|
||||
if request.uri().path().starts_with("/api") {
|
||||
self.api.call(request)
|
||||
} else {
|
||||
self.ui.call(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RootService {
|
||||
pub fn new(serve_session: Arc<ServeSession>) -> Self {
|
||||
RootService {
|
||||
api: ApiService::new(Arc::clone(&serve_session)),
|
||||
ui: UiService::new(Arc::clone(&serve_session)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LiveServer {
|
||||
serve_session: Arc<ServeSession>,
|
||||
}
|
||||
@@ -58,14 +31,31 @@ impl LiveServer {
|
||||
}
|
||||
|
||||
pub fn start(self, address: SocketAddr) {
|
||||
let server = Server::bind(&address)
|
||||
.serve(move || {
|
||||
let service: FutureResult<_, hyper::Error> =
|
||||
future::ok(RootService::new(Arc::clone(&self.serve_session)));
|
||||
service
|
||||
})
|
||||
.map_err(|e| eprintln!("Server error: {}", e));
|
||||
let serve_session = Arc::clone(&self.serve_session);
|
||||
|
||||
hyper::rt::run(server);
|
||||
let make_service = make_service_fn(move |_conn| {
|
||||
let serve_session = Arc::clone(&serve_session);
|
||||
|
||||
async {
|
||||
let service = move |req: Request<Body>| {
|
||||
let serve_session = Arc::clone(&serve_session);
|
||||
|
||||
async move {
|
||||
if req.uri().path().starts_with("/api") {
|
||||
Ok::<_, Infallible>(api::call(serve_session, req).await)
|
||||
} else {
|
||||
Ok::<_, Infallible>(ui::call(serve_session, req).await)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok::<_, Infallible>(service_fn(service))
|
||||
}
|
||||
});
|
||||
|
||||
let rt = Runtime::new().unwrap();
|
||||
let _guard = rt.enter();
|
||||
let server = Server::bind(&address).serve(make_service);
|
||||
rt.block_on(server).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//! Defines the HTTP-based UI. These endpoints generally return HTML and SVG.
|
||||
//! Defines the HTTP-based Rojo UI. It uses ritz for templating, which is like
|
||||
//! JSX for Rust. Eventually we should probably replace this with a new
|
||||
//! framework, maybe using JS and client side rendering.
|
||||
//!
|
||||
//! These endpoints generally return HTML and SVG.
|
||||
|
||||
use std::{borrow::Cow, sync::Arc, time::Duration};
|
||||
|
||||
use futures::{future, Future};
|
||||
use hyper::{header, service::Service, Body, Method, Request, Response, StatusCode};
|
||||
use hyper::{header, Body, Method, Request, Response, StatusCode};
|
||||
use maplit::hashmap;
|
||||
use rbx_dom_weak::types::{Ref, Variant};
|
||||
use ritz::{html, Fragment, HtmlContent, HtmlSelfClosingTag};
|
||||
@@ -18,32 +21,23 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
pub struct UiService {
|
||||
serve_session: Arc<ServeSession>,
|
||||
pub async fn call(serve_session: Arc<ServeSession>, request: Request<Body>) -> Response<Body> {
|
||||
let service = UiService::new(serve_session);
|
||||
|
||||
match (request.method(), request.uri().path()) {
|
||||
(&Method::GET, "/") => service.handle_home(),
|
||||
(&Method::GET, "/logo.png") => service.handle_logo(),
|
||||
(&Method::GET, "/icon.png") => service.handle_icon(),
|
||||
(&Method::GET, "/show-instances") => service.handle_show_instances(),
|
||||
(_method, path) => json(
|
||||
ErrorResponse::not_found(format!("Route not found: {}", path)),
|
||||
StatusCode::NOT_FOUND,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
impl Service for UiService {
|
||||
type ReqBody = Body;
|
||||
type ResBody = Body;
|
||||
type Error = hyper::Error;
|
||||
type Future = Box<dyn Future<Item = Response<Self::ReqBody>, Error = Self::Error> + Send>;
|
||||
|
||||
fn call(&mut self, request: Request<Self::ReqBody>) -> Self::Future {
|
||||
let response = match (request.method(), request.uri().path()) {
|
||||
(&Method::GET, "/") => self.handle_home(),
|
||||
(&Method::GET, "/logo.png") => self.handle_logo(),
|
||||
(&Method::GET, "/icon.png") => self.handle_icon(),
|
||||
(&Method::GET, "/show-instances") => self.handle_show_instances(),
|
||||
(_method, path) => {
|
||||
return json(
|
||||
ErrorResponse::not_found(format!("Route not found: {}", path)),
|
||||
StatusCode::NOT_FOUND,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
Box::new(future::ok(response))
|
||||
}
|
||||
pub struct UiService {
|
||||
serve_session: Arc<ServeSession>,
|
||||
}
|
||||
|
||||
impl UiService {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use futures::{future, Future};
|
||||
use hyper::{header::CONTENT_TYPE, Body, Response, StatusCode};
|
||||
use serde::Serialize;
|
||||
|
||||
fn response_json<T: Serialize>(value: T, code: StatusCode) -> Response<Body> {
|
||||
pub fn json_ok<T: Serialize>(value: T) -> Response<Body> {
|
||||
json(value, StatusCode::OK)
|
||||
}
|
||||
|
||||
pub fn json<T: Serialize>(value: T, code: StatusCode) -> Response<Body> {
|
||||
let serialized = match serde_json::to_string(&value) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
@@ -20,16 +23,3 @@ fn response_json<T: Serialize>(value: T, code: StatusCode) -> Response<Body> {
|
||||
.body(Body::from(serialized))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn json<T: Serialize>(
|
||||
value: T,
|
||||
code: StatusCode,
|
||||
) -> Box<dyn Future<Item = hyper::Response<hyper::Body>, Error = hyper::Error> + Send> {
|
||||
Box::new(future::ok(response_json(value, code)))
|
||||
}
|
||||
|
||||
pub fn json_ok<T: Serialize>(
|
||||
value: T,
|
||||
) -> Box<dyn Future<Item = hyper::Response<hyper::Body>, Error = hyper::Error> + Send> {
|
||||
json(value, StatusCode::OK)
|
||||
}
|
||||
|
||||
12
test-projects/weldconstraint/default.project.json
Normal file
12
test-projects/weldconstraint/default.project.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "weldconstraint",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
|
||||
"Workspace": {
|
||||
"Parts": {
|
||||
"$path": "two-parts-welded.rbxmx"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
224
test-projects/weldconstraint/two-parts-welded.rbxmx
Normal file
224
test-projects/weldconstraint/two-parts-welded.rbxmx
Normal file
@@ -0,0 +1,224 @@
|
||||
<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
|
||||
<Meta name="ExplicitAutoJoints">true</Meta>
|
||||
<External>null</External>
|
||||
<External>nil</External>
|
||||
<Item class="Folder" referent="RBX1959D8B589424CFD943B349BB8DB0A3B">
|
||||
<Properties>
|
||||
<BinaryString name="AttributesSerialize"></BinaryString>
|
||||
<string name="Name">Folder</string>
|
||||
<int64 name="SourceAssetId">-1</int64>
|
||||
<BinaryString name="Tags"></BinaryString>
|
||||
</Properties>
|
||||
<Item class="Part" referent="RBX15D09A13EACB4A6D96E75739B60CB129">
|
||||
<Properties>
|
||||
<bool name="Anchored">false</bool>
|
||||
<BinaryString name="AttributesSerialize"></BinaryString>
|
||||
<float name="BackParamA">-0.5</float>
|
||||
<float name="BackParamB">0.5</float>
|
||||
<token name="BackSurface">0</token>
|
||||
<token name="BackSurfaceInput">0</token>
|
||||
<float name="BottomParamA">-0.5</float>
|
||||
<float name="BottomParamB">0.5</float>
|
||||
<token name="BottomSurface">0</token>
|
||||
<token name="BottomSurfaceInput">0</token>
|
||||
<CoordinateFrame name="CFrame">
|
||||
<X>-14</X>
|
||||
<Y>0.5</Y>
|
||||
<Z>-5</Z>
|
||||
<R00>1</R00>
|
||||
<R01>0</R01>
|
||||
<R02>0</R02>
|
||||
<R10>0</R10>
|
||||
<R11>1</R11>
|
||||
<R12>0</R12>
|
||||
<R20>0</R20>
|
||||
<R21>0</R21>
|
||||
<R22>1</R22>
|
||||
</CoordinateFrame>
|
||||
<bool name="CanCollide">true</bool>
|
||||
<bool name="CanQuery">true</bool>
|
||||
<bool name="CanTouch">true</bool>
|
||||
<bool name="CastShadow">true</bool>
|
||||
<int name="CollisionGroupId">0</int>
|
||||
<Color3uint8 name="Color3uint8">4288914085</Color3uint8>
|
||||
<PhysicalProperties name="CustomPhysicalProperties">
|
||||
<CustomPhysics>false</CustomPhysics>
|
||||
</PhysicalProperties>
|
||||
<float name="FrontParamA">-0.5</float>
|
||||
<float name="FrontParamB">0.5</float>
|
||||
<token name="FrontSurface">0</token>
|
||||
<token name="FrontSurfaceInput">0</token>
|
||||
<float name="LeftParamA">-0.5</float>
|
||||
<float name="LeftParamB">0.5</float>
|
||||
<token name="LeftSurface">0</token>
|
||||
<token name="LeftSurfaceInput">0</token>
|
||||
<bool name="Locked">false</bool>
|
||||
<bool name="Massless">false</bool>
|
||||
<token name="Material">256</token>
|
||||
<string name="Name">A</string>
|
||||
<CoordinateFrame name="PivotOffset">
|
||||
<X>0</X>
|
||||
<Y>0</Y>
|
||||
<Z>0</Z>
|
||||
<R00>1</R00>
|
||||
<R01>0</R01>
|
||||
<R02>0</R02>
|
||||
<R10>0</R10>
|
||||
<R11>1</R11>
|
||||
<R12>0</R12>
|
||||
<R20>0</R20>
|
||||
<R21>0</R21>
|
||||
<R22>1</R22>
|
||||
</CoordinateFrame>
|
||||
<float name="Reflectance">0</float>
|
||||
<float name="RightParamA">-0.5</float>
|
||||
<float name="RightParamB">0.5</float>
|
||||
<token name="RightSurface">0</token>
|
||||
<token name="RightSurfaceInput">0</token>
|
||||
<int name="RootPriority">0</int>
|
||||
<Vector3 name="RotVelocity">
|
||||
<X>0</X>
|
||||
<Y>0</Y>
|
||||
<Z>0</Z>
|
||||
</Vector3>
|
||||
<int64 name="SourceAssetId">-1</int64>
|
||||
<BinaryString name="Tags"></BinaryString>
|
||||
<float name="TopParamA">-0.5</float>
|
||||
<float name="TopParamB">0.5</float>
|
||||
<token name="TopSurface">0</token>
|
||||
<token name="TopSurfaceInput">0</token>
|
||||
<float name="Transparency">0</float>
|
||||
<Vector3 name="Velocity">
|
||||
<X>0</X>
|
||||
<Y>0</Y>
|
||||
<Z>0</Z>
|
||||
</Vector3>
|
||||
<token name="formFactorRaw">1</token>
|
||||
<token name="shape">1</token>
|
||||
<Vector3 name="size">
|
||||
<X>4</X>
|
||||
<Y>1</Y>
|
||||
<Z>2</Z>
|
||||
</Vector3>
|
||||
</Properties>
|
||||
<Item class="WeldConstraint" referent="RBXD0337E67C9F1411681C2FEC8CA324E6F">
|
||||
<Properties>
|
||||
<BinaryString name="AttributesSerialize"></BinaryString>
|
||||
<CoordinateFrame name="CFrame0">
|
||||
<X>7</X>
|
||||
<Y>1.01327896e-06</Y>
|
||||
<Z>-3</Z>
|
||||
<R00>1</R00>
|
||||
<R01>0</R01>
|
||||
<R02>0</R02>
|
||||
<R10>0</R10>
|
||||
<R11>1</R11>
|
||||
<R12>0</R12>
|
||||
<R20>0</R20>
|
||||
<R21>0</R21>
|
||||
<R22>1</R22>
|
||||
</CoordinateFrame>
|
||||
<string name="Name">WeldConstraint</string>
|
||||
<Ref name="Part0Internal">RBX15D09A13EACB4A6D96E75739B60CB129</Ref>
|
||||
<Ref name="Part1Internal">RBX308EE5932F7A492685067C0B84AA3DAF</Ref>
|
||||
<int64 name="SourceAssetId">-1</int64>
|
||||
<int name="State">3</int>
|
||||
<BinaryString name="Tags"></BinaryString>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
<Item class="Part" referent="RBX308EE5932F7A492685067C0B84AA3DAF">
|
||||
<Properties>
|
||||
<bool name="Anchored">false</bool>
|
||||
<BinaryString name="AttributesSerialize"></BinaryString>
|
||||
<float name="BackParamA">-0.5</float>
|
||||
<float name="BackParamB">0.5</float>
|
||||
<token name="BackSurface">0</token>
|
||||
<token name="BackSurfaceInput">0</token>
|
||||
<float name="BottomParamA">-0.5</float>
|
||||
<float name="BottomParamB">0.5</float>
|
||||
<token name="BottomSurface">0</token>
|
||||
<token name="BottomSurfaceInput">0</token>
|
||||
<CoordinateFrame name="CFrame">
|
||||
<X>-7</X>
|
||||
<Y>0.500001013</Y>
|
||||
<Z>-8</Z>
|
||||
<R00>1</R00>
|
||||
<R01>0</R01>
|
||||
<R02>0</R02>
|
||||
<R10>0</R10>
|
||||
<R11>1</R11>
|
||||
<R12>0</R12>
|
||||
<R20>0</R20>
|
||||
<R21>0</R21>
|
||||
<R22>1</R22>
|
||||
</CoordinateFrame>
|
||||
<bool name="CanCollide">true</bool>
|
||||
<bool name="CanQuery">true</bool>
|
||||
<bool name="CanTouch">true</bool>
|
||||
<bool name="CastShadow">true</bool>
|
||||
<int name="CollisionGroupId">0</int>
|
||||
<Color3uint8 name="Color3uint8">4288914085</Color3uint8>
|
||||
<PhysicalProperties name="CustomPhysicalProperties">
|
||||
<CustomPhysics>false</CustomPhysics>
|
||||
</PhysicalProperties>
|
||||
<float name="FrontParamA">-0.5</float>
|
||||
<float name="FrontParamB">0.5</float>
|
||||
<token name="FrontSurface">0</token>
|
||||
<token name="FrontSurfaceInput">0</token>
|
||||
<float name="LeftParamA">-0.5</float>
|
||||
<float name="LeftParamB">0.5</float>
|
||||
<token name="LeftSurface">0</token>
|
||||
<token name="LeftSurfaceInput">0</token>
|
||||
<bool name="Locked">false</bool>
|
||||
<bool name="Massless">false</bool>
|
||||
<token name="Material">256</token>
|
||||
<string name="Name">B</string>
|
||||
<CoordinateFrame name="PivotOffset">
|
||||
<X>0</X>
|
||||
<Y>0</Y>
|
||||
<Z>0</Z>
|
||||
<R00>1</R00>
|
||||
<R01>0</R01>
|
||||
<R02>0</R02>
|
||||
<R10>0</R10>
|
||||
<R11>1</R11>
|
||||
<R12>0</R12>
|
||||
<R20>0</R20>
|
||||
<R21>0</R21>
|
||||
<R22>1</R22>
|
||||
</CoordinateFrame>
|
||||
<float name="Reflectance">0</float>
|
||||
<float name="RightParamA">-0.5</float>
|
||||
<float name="RightParamB">0.5</float>
|
||||
<token name="RightSurface">0</token>
|
||||
<token name="RightSurfaceInput">0</token>
|
||||
<int name="RootPriority">0</int>
|
||||
<Vector3 name="RotVelocity">
|
||||
<X>0</X>
|
||||
<Y>0</Y>
|
||||
<Z>0</Z>
|
||||
</Vector3>
|
||||
<int64 name="SourceAssetId">-1</int64>
|
||||
<BinaryString name="Tags"></BinaryString>
|
||||
<float name="TopParamA">-0.5</float>
|
||||
<float name="TopParamB">0.5</float>
|
||||
<token name="TopSurface">0</token>
|
||||
<token name="TopSurfaceInput">0</token>
|
||||
<float name="Transparency">0</float>
|
||||
<Vector3 name="Velocity">
|
||||
<X>0</X>
|
||||
<Y>0</Y>
|
||||
<Z>0</Z>
|
||||
</Vector3>
|
||||
<token name="formFactorRaw">1</token>
|
||||
<token name="shape">1</token>
|
||||
<Vector3 name="size">
|
||||
<X>4</X>
|
||||
<Y>1</Y>
|
||||
<Z>2</Z>
|
||||
</Vector3>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
</roblox>
|
||||
@@ -34,7 +34,7 @@ pub fn copy_recursive(from: &Path, to: &Path) -> io::Result<()> {
|
||||
Ok(_) => {}
|
||||
Err(err) => match err.kind() {
|
||||
io::ErrorKind::AlreadyExists => {}
|
||||
_ => panic!(err),
|
||||
_ => return Err(err),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user