Compare commits

...

30 Commits

Author SHA1 Message Date
Lucien Greathouse
af4a3ca0af Update memofs to 0.2.0 2021-08-23 16:00:51 -04:00
Lucien Greathouse
43715143e4 Release v7.0.0-rc.1 2021-08-23 15:50:28 -04:00
Lucien Greathouse
16aa354d36 Update dependencies and fix Lua ref tests 2021-08-23 15:48:17 -04:00
Lucien Greathouse
c739025453 Update changelog 2021-08-23 15:23:19 -04:00
James Onnen
f0526d17de Support long file paths on Windows (past 256 limit) (#464)
* Support long file paths on Windows (past 256 limit)

This issue can occur when using symlinks deep in rojo such that very long paths can occur, among other scenarios.

Note while the original fix comes from here:
	https://gal.hagever.com/posts/windows-long-paths-in-rust/

The manifest had to be modified from this source:
	https://stackoverflow.com/questions/59816045/windows-10-1903-longpathaware-not-working

* Move manifests, tidy code a little

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2021-08-23 15:21:01 -04:00
Ayuka
6cc2e919c0 Update nil Ref check and property decode warning to new Rojo protocol (#466)
* Skip empty Refs in new Rojo protocol

* Update warning message for new Rojo protocol
2021-08-09 14:14:59 -04:00
Kenneth Loeffler
e1f9eaefa9 Fix Ref reification (#462)
* Update ApiValue type to use new value format

* Use new value format for Ref reification
2021-08-05 03:03:13 -04:00
Lucien Greathouse
5d62bf9b60 Upgrade to Tokio 1.x, futures 0.3, Hyper 0.14, etc (#459)
* Upgrade dependencies, oneshot channel ref

* New service style?

* Fix warning

* A server is running again

* Working server with async blocks

* UI working again

* Finish upgrade

* Bump MSRV to 1.46.0 for if/match in const fn

* Update the README as part of this
2021-07-28 12:29:46 -04:00
Lucien Greathouse
4aa5814a0a Update dependencies 2021-07-27 14:06:23 -04:00
Lucien Greathouse
5bca244062 Add test project for #458 2021-07-27 12:42:47 -04:00
Lucien Greathouse
7cf57714a4 Use new Cargo config.toml convention 2021-07-12 13:19:36 -04:00
Lucien Greathouse
92e6f862ad Update plugin to use new property format 2021-07-02 16:12:12 -04:00
Lucien Greathouse
2377f41036 Update to latest rbx-dom 2.0 dependencies, including Lua 2021-07-02 16:04:43 -04:00
Lucien Greathouse
26a08f4d9f Update Changelog 2021-06-29 01:26:29 -04:00
Lucien Greathouse
672d207961 Update to stable rbx-dom libraries 2021-06-29 01:20:09 -04:00
Lucien Greathouse
a3d8e50f26 Fix changelog bullets being in the wrong section 2021-06-18 16:47:52 -04:00
Lucien Greathouse
d3abca46a8 Fix deprecation warning by writing better code 2021-06-14 12:50:54 -04:00
Lucien Greathouse
17fdd18c55 Code docs 2021-06-11 22:19:50 -04:00
Lucien Greathouse
e482d038c6 Tests for value resolution, better errors, no more Color3uint8 2021-06-11 22:04:04 -04:00
Lucien Greathouse
d0482a004e Modernize upload command 2021-06-08 21:53:56 -04:00
Lucien Greathouse
561a3e3256 Modernize serve subcommand 2021-06-08 17:05:55 -04:00
Lucien Greathouse
158dac5e1c Move subcommand branching into Options struct 2021-06-08 16:53:03 -04:00
Lucien Greathouse
1413f8c0b6 Tidy up build command further 2021-06-08 16:50:21 -04:00
Lucien Greathouse
ffb2aa332a Modernize build subcommand 2021-06-08 16:48:20 -04:00
Lucien Greathouse
45e8208e9c Improve CLI help text 2021-06-08 15:37:59 -04:00
Lucien Greathouse
7f230a8bf4 Modernize the plugin subcommand 2021-05-21 13:09:07 -04:00
Lucien Greathouse
afe26b8c16 Modernize the doc subcommand 2021-05-21 12:45:07 -04:00
Lucien Greathouse
d153f62b8a Modernize the init subcommand 2021-05-20 17:34:45 -04:00
Lucien Greathouse
5c80cd6e50 Skip serializing place_id and game_id if null 2021-05-20 15:46:40 -04:00
Lucien Greathouse
df1aced95d Add fmt-project subcommand 2021-05-20 15:41:08 -04:00
80 changed files with 7578 additions and 7251 deletions

View File

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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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>&nbsp;</div>
<div align="center">
<a href="https://github.com/rojo-rbx/rojo/actions">
<img src="https://github.com/rojo-rbx/rojo/workflows/CI/badge.svg" alt="Actions status" />
</a>
<a href="https://crates.io/crates/rojo">
<img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" />
</a>
<a href="https://rojo.space/docs">
<img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" />
</a>
<a href="https://github.com/rojo-rbx/rojo/actions"><img src="https://github.com/rojo-rbx/rojo/workflows/CI/badge.svg" alt="Actions status" /></a>
<a href="https://crates.io/crates/rojo"><img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" /></a>
<a href="https://rojo.space/docs"><img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" /></a>
</div>
<hr />
@@ -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.

View File

@@ -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(())
}

View File

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

View 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>

View File

@@ -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.

View File

@@ -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"] }

View File

@@ -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

View File

@@ -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

View File

@@ -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),
}
}

View File

@@ -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",

View File

@@ -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",
},
},
})

View File

@@ -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

View File

@@ -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

View File

@@ -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 = {},

View File

@@ -70,7 +70,7 @@ function reifyInner(instanceMap, virtualInstances, id, parentInstance, unapplied
for propertyName, virtualValue in pairs(virtualInstance.Properties) do
-- Because refs may refer to instances that we haven't constructed yet,
-- we defer applying any ref properties until all instances are created.
if 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

View File

@@ -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"},

View File

@@ -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,
})
})

View File

@@ -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">

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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.
///

View File

@@ -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(())
}

View File

@@ -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
View 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(())
}
}

View File

@@ -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()),
}
}

View File

@@ -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,
}

View File

@@ -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);

View File

@@ -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())?;

View File

@@ -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(())

View File

@@ -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},

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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)),
);
}
}

View File

@@ -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

View File

@@ -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: ~

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: ~

View File

@@ -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,

View File

@@ -360,8 +360,7 @@ mod test {
"$className": "StringValue",
"$properties": {
"Value": {
"Type": "String",
"Value": "Hello, world!"
"String": "Hello, world!"
}
}
}

View File

@@ -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();

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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};

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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();
}
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -0,0 +1,12 @@
{
"name": "weldconstraint",
"tree": {
"$className": "DataModel",
"Workspace": {
"Parts": {
"$path": "two-parts-welded.rbxmx"
}
}
}
}

View 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>

View File

@@ -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 {