Compare commits

...

78 Commits

Author SHA1 Message Date
Lucien Greathouse
b9ed68fa9e Release v7.1.1 2022-05-26 02:53:20 -04:00
Lucien Greathouse
6c6d6c9c8d Add .github/FUNDING.yml 2022-05-26 02:28:57 -04:00
Lucien Greathouse
e169d7be68 New release workflow (#547)
* Port release workflow from Aftman to test

* Checkout submodules in plugin build step

* ...and build with submodules for other builds too

* Fix ci.yml; we use master branch still

* CI with submodules too
2022-05-25 22:26:22 -04:00
Lucien Greathouse
192fd7d4dd New and improved CI pipeline 2022-05-25 18:53:08 -04:00
Lucien Greathouse
1f1193e857 Remove unused lazy_static 2022-05-25 18:48:57 -04:00
boatbomber
0a412ade88 Remove duplicate PluginSettings.StudioProvider (#545) 2022-05-25 18:48:10 -04:00
Filip Tibell
3cef2fe9aa Fix sourcemap command not stripping paths correctly (#544)
* Fix sourcemap command not stripping paths correctly

* Use ServeSession to get the proper root dir to strip for sourcemap
2022-05-23 15:19:30 -04:00
Lucien Greathouse
18e53f06fe Remove unused dependencies and dead code warnings 2022-05-22 19:20:41 -04:00
Lucien Greathouse
eaac539087 Update to reqwest 0.11.10 2022-05-22 19:16:43 -04:00
Lucien Greathouse
57005c4fd5 Update uuid and winreg 2022-05-22 19:13:11 -04:00
Lucien Greathouse
ea58999a2a Update to pretty_assertions 1.2.1 2022-05-22 19:12:33 -04:00
Lucien Greathouse
a5a69fd9fc Release v7.1.0 2022-05-22 18:53:45 -04:00
boatbomber
f1d0f1c1c9 Bugfix: PluginAction spam causing errors (#541)
* Use session's state instead of existence to determine action

* Retain host/port text

* Use bindings instead of text/ref tunneling

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-05-22 18:47:11 -04:00
boatbomber
83492d7495 PluginActions for connecting/disconnecting a session (#537)
* Create plugin action component

* Add plugin action for session start/end

* Add output for connection status change

* Move host & port refs to App level so keybind can access them

* Use passed function directly

* Improve the action text clarity

* Add actions for single action

* Add to changelog

* Explicitly return nil

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>

* Change log level to info

* Refactor startSession to contain the logic

* Formatting

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-05-02 17:49:31 -04:00
boatbomber
10abc2254a Add changing toolbar icon to indicate state (#538)
* Add changing toolbar icon

* Return to default icon after closing error

* Update changelog

* Add assets

* Improved link icon

* Upload new icons

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-05-02 17:28:18 -04:00
Lucien Greathouse
5d5536a95e Update CHANGELOG 2022-04-19 18:45:09 -04:00
James Onnen
fe81e55925 Add support for optional paths (#472)
* Add PathNode with optional fields to project. This allows a path to be defined either as `"$path": "src"` or `"$path": { "optional": "src" }`

* Make $path truly optional

* Prevent rojo from erroring if no project node is resolved

* Use match instead of if-statement

* Add end-to-end tests (credit to MobiusCraftFlip for initial scenario)

* Pass option with ref inside instead of reference to option

* Empty commit to restart GitHub Actions

* Simplify build test

* Minimize serve test: it fails

* Simplify serve test even more

* Ignore failing serve test

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-04-19 18:43:47 -04:00
Micah
654690d73e Add testez std (#535) 2022-04-19 17:45:55 -04:00
Filip Tibell
256aba4bc1 Implement sourcemap CLI command (#530)
* Initial implementation of sourcemap CLI command

* Update src/cli/sourcemap.rs

Co-authored-by: JohnnyMorganz <johnnymorganz@outlook.com>

* Update src/cli/sourcemap.rs

Co-authored-by: JohnnyMorganz <johnnymorganz@outlook.com>

* Tidy up sourcemap command

* Update CHANGELOG

Co-authored-by: JohnnyMorganz <johnnymorganz@outlook.com>
Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-04-19 17:45:29 -04:00
Ashton Miller
49f8845105 Add ability to specify address in default.project.json (#507)
* Allow for setting the default port in project json

set as
```json
"serveAddress": "0.0.0.0"
```

* Update CHANGELOG.md

* cargo fmt

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-04-19 16:54:03 -04:00
Ashton Miller
12370846b4 Support the new Open Cloud API (#504)
* Add support for the new Open Cloud API

* Cleanup Open Cloud variables

* Avoid cloning buffer for do_upload_open_cloud

* Satisfy cargo fmt

* Actually correct cargo fmt

Apparently my earlier fix did not fix everything.

* Update CHANGELOG.md

* Update CHANGELOG.md

Forgot to add the link to issue #486 in the previous commit :/

* Cleanup & improve code for open cloud api

* Commit to force GH Actions to run (?)

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-04-19 16:02:40 -04:00
Lucien Greathouse
07637dfe96 Release v7.0.0 2021-12-10 19:37:39 -05:00
Lucien Greathouse
f389a4a1db Update rbx_dom_lua 2021-11-26 17:31:12 -05:00
Lucien Greathouse
af077c796c Update dependencies 2021-11-26 17:28:13 -05:00
Lucien Greathouse
1c319f2fa8 Promote weldconstraint test project to a build test 2021-11-26 16:36:55 -05:00
Lucien Greathouse
e8afa03f7b Fix most test output (but not termcolor) 2021-11-22 13:59:12 -05:00
Lucien Greathouse
9b22545842 Add tests for current file naming with regards to project name field 2021-11-22 13:22:16 -05:00
Lucien Greathouse
adc733d25c Update changelog 2021-11-20 18:14:17 -05:00
Lucien Greathouse
6896257647 Bump MSRV to 1.55.0 2021-11-20 18:07:24 -05:00
Lucien Greathouse
1d9845a6cb Update dependencies 2021-11-20 18:06:41 -05:00
Blake Mealey
8461339e9a Add note for git submodules (#495) 2021-11-20 17:53:22 -05:00
Umbreon
9904d94e4c Remember sync connection settings. (#500) 2021-11-20 17:51:38 -05:00
Lucien Greathouse
da25c80d0b Add support for CFrame shorthand. Fixes #430. 2021-11-20 17:50:40 -05:00
Lucien Greathouse
5fa63733fd Factor out property filtering code to simplify web server 2021-11-20 17:38:36 -05:00
Lucien Greathouse
8b54bf0ba1 Improve error when file is not found 2021-11-20 17:15:58 -05:00
Lucien Greathouse
173dc12cb3 Improve warning and debug output in plugin 2021-11-20 17:05:45 -05:00
Umbreon
e136529ff0 Add a check to getProperty for unknown properties. (#493) 2021-10-28 01:09:20 -04:00
Lucien Greathouse
75542dacb3 Release Rojo 7.0.0-rc.3 2021-10-19 17:12:28 -04:00
Lucien Greathouse
07abfbde43 Release Rojo 7.0.0-rc.2 2021-10-19 17:07:14 -04:00
Kenneth Loeffler
96112fe118 Add ambiguous value resolution StringArray -> Tags (#484)
* Add ambiguous value resolution StringArray -> Tags

* Remove funny autocompleted reference
2021-10-19 16:46:31 -04:00
Kenneth Loeffler
9d0b313261 Add ChangeBatcher to plugin for two-way sync (#478)
* Implement ChangeBatcher

* Use ChangeBatcher for two-way sync

* Pause updates during patch application

* I can English good

* Break after encountering a nil Parent change

This prevents __flush from erroring out when an instance's Parent is
changed to nil and it has other property changes in the same batch.

* Update rbx_dom_lua

* Don't connect changed listeners in a running game

 #468 made me realize how bad of an idea this is in general...

* Update TestEZ and fix sibling Ref reification test

* Add ChangeBatcher tests

* Test instance unpausing by breaking functionality out to __cycle

* Break up the module a bit and improve tests

* Shuffle requires around and edit comment

* Break out more stuff, rename createChangePatch -> createPatchSet

* Make ChangeBatcher responsible for unpausing all paused instances

This somewhat improves the situation (of course, it would preferrable
to not have to hack around this problem with Source at all). It also
sets us up nicely if we come across any other properties that do
anything similar.

* Remove old reference to pausedBatchInstances

* Use RenderStepped instead of Heartbeat and trash multi-frame pauses

I probably should have done this in the first place...

ChangeBatcher still needs to unpause instances, but we don't need to
hold pauses for any longer than one cycle.

* Remove useless branch

* if not next(x) -> if next(x) == nil

* Add InstanceMap:unpauseAllInstances, use it in ChangeBatcher

* Move IsRunning check to InstanceMap:__maybeFireInstanceChanged
2021-10-18 18:18:51 -04:00
Wiktor Rudnicki
277ddfa9be Themes colors modification (#482)
* Modified colors of themes

Colors match Roblox Studio theme.

* Change cases of colors

Colors' hexes have correct cases now.
2021-10-15 13:27:47 -04:00
Lucien Greathouse
5d88bdb256 Upgrade dependencies in lockfile 2021-10-15 13:24:40 -04:00
Lucien Greathouse
8d29b43155 Upgrade dependencies 2021-10-11 17:40:14 -04:00
Lucien Greathouse
cc071a6415 Move entrypoint from src/bin.rs to src/main.rs 2021-09-14 20:42:38 -04:00
Lucien Greathouse
8954def25c Move responsibility for extracting names from paths lower 2021-08-24 17:59:53 -04:00
Lucien Greathouse
d484098781 Get rid of confusing 'SnapshotInstanceResult' type alias 2021-08-24 17:15:47 -04:00
Lucien Greathouse
9f06cbf3a0 Update contributing guide 2021-08-23 16:11:56 -04:00
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
144 changed files with 13471 additions and 8746 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
patreon: lpghatguy

View File

@@ -11,29 +11,49 @@ on:
jobs: jobs:
build: build:
name: Build and Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
rust_version: [stable, "1.43.1"] rust_version: [stable, 1.55.0]
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Setup Rust toolchain - name: Install Rust
run: rustup default ${{ matrix.rust_version }} uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust_version }}
override: true
profile: minimal
- name: Build - name: Build
run: cargo build --locked --verbose run: cargo build --locked --verbose
- name: Run tests - name: Test
run: cargo test --locked --verbose run: cargo test --locked --verbose
- name: Rustfmt and Clippy lint:
run: | name: Rustfmt and Clippy
cargo fmt -- --check runs-on: ubuntu-latest
cargo clippy
if: matrix.rust_version == 'stable' steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Rustfmt
run: cargo fmt -- --check
- name: Clippy
run: cargo clippy

View File

@@ -2,65 +2,152 @@ name: Release
on: on:
push: push:
tags: ["*"] tags: ["v*"]
jobs: jobs:
windows: create-release:
runs-on: windows-latest name: Create Release
steps:
- uses: actions/checkout@v1
with:
submodules: true
- name: Build release binary
run: cargo build --verbose --locked --release
- name: Upload artifacts
uses: actions/upload-artifact@v1
with:
name: rojo-win64
path: target/release/rojo.exe
macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v1
with:
submodules: true
- name: Install Rust
run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
- name: Build release binary
run: |
source $HOME/.cargo/env
cargo build --verbose --locked --release
env:
OPENSSL_STATIC: 1
- name: Upload artifacts
uses: actions/upload-artifact@v1
with:
name: rojo-macos
path: target/release/rojo
linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps: steps:
- uses: actions/checkout@v1 - name: Create Release
with: id: create_release
submodules: true uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
prerelease: false
- name: Build build-plugin:
run: cargo build --locked --verbose --release needs: ["create-release"]
env: name: Build Roblox Studio Plugin
OPENSSL_STATIC: 1 runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Upload artifacts - name: Setup Foreman
uses: actions/upload-artifact@v1 uses: Roblox/setup-foreman@v1
with: with:
name: rojo-linux token: ${{ secrets.GITHUB_TOKEN }}
path: target/release/rojo
- name: Build Plugin
run: rojo build plugin --output Rojo.rbxm
- name: Upload Plugin to Release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: Rojo.rbxm
asset_name: Rojo.rbxm
asset_content_type: application/octet-stream
- name: Upload Plugin to Artifacts
uses: actions/upload-artifact@v3
with:
name: Rojo.rbxm
path: Rojo.rbxm
build:
needs: ["create-release"]
strategy:
fail-fast: false
matrix:
# https://doc.rust-lang.org/rustc/platform-support.html
#
# FIXME: After the Rojo VS Code extension updates, add architecture
# names to each of these releases. We'll rename win64 to windows and add
# -x86_64 to each release.
include:
- host: linux
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
label: linux
- host: windows
os: windows-latest
target: x86_64-pc-windows-msvc
label: win64
- host: macos
os: macos-latest
target: x86_64-apple-darwin
label: macos
- host: macos
os: macos-latest
target: aarch64-apple-darwin
label: macos-aarch64
name: Build (${{ matrix.target }})
runs-on: ${{ matrix.os }}
env:
BIN: rojo
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Get Version from Tag
shell: bash
# https://github.community/t/how-to-get-just-the-tag-name/16241/7#M1027
run: |
echo "PROJECT_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
echo "Version is: ${{ env.PROJECT_VERSION }}"
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: ${{ matrix.target }}
override: true
profile: minimal
- name: Build Release
run: cargo build --release --locked --verbose
env:
# Build into a known directory so we can find our build artifact more
# easily.
CARGO_TARGET_DIR: output
# On platforms that use OpenSSL, ensure it is statically linked to
# make binaries more portable.
OPENSSL_STATIC: 1
- name: Create Release Archive
shell: bash
run: |
mkdir staging
if [ "${{ matrix.host }}" = "windows" ]; then
cp "output/release/$BIN.exe" staging/
cd staging
7z a ../release.zip *
else
cp "output/release/$BIN" staging/
cd staging
zip ../release.zip *
fi
- name: Upload Archive to Release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: release.zip
asset_name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
asset_content_type: application/octet-stream
- name: Upload Archive to Artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
path: release.zip

View File

@@ -2,10 +2,116 @@
## Unreleased Changes ## Unreleased Changes
## [7.1.1] - May 26, 2022
* Fixed sourcemap command not stripping paths correctly ([#544])
* Fixed Studio plugin settings not saving correctly.
[#544]: https://github.com/rojo-rbx/rojo/pull/544
[#545]: https://github.com/rojo-rbx/rojo/pull/545
[7.1.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.1.1
## [7.1.0] - May 22, 2022
* Added support for specifying an address to be used by default in project files. ([#507])
* Added support for optional paths in project files. ([#472])
* Added support for the new Open Cloud API when uploading. ([#504])
* Added `sourcemap` command for generating sourcemaps to feed into other tools. ([#530])
* Added PluginActions for connecting/disconnecting a session ([#537])
* Added changing toolbar icon to indicate state ([#538])
[#472]: https://github.com/rojo-rbx/rojo/pull/472
[#504]: https://github.com/rojo-rbx/rojo/pull/504
[#507]: https://github.com/rojo-rbx/rojo/pull/507
[#530]: https://github.com/rojo-rbx/rojo/pull/530
[#537]: https://github.com/rojo-rbx/rojo/pull/537
[#538]: https://github.com/rojo-rbx/rojo/pull/538
[7.1.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.1.0
## [7.0.0] - December 10, 2021
* Fixed Rojo's interactions with properties enabled by FFlags that are not yet enabled. ([#493])
* Improved output in Roblox Studio plugin when bad property data is encountered.
* Reintroduced support for CFrame shorthand syntax in Rojo project and `.meta.json` files, matching Rojo 6. ([#430])
* Connection settings are now remembered when reconnecting in Roblox Studio. ([#500])
* Updated reflection database to Roblox v503.
[#430]: https://github.com/rojo-rbx/rojo/issues/430
[#493]: https://github.com/rojo-rbx/rojo/pull/493
[#500]: https://github.com/rojo-rbx/rojo/pull/500
[7.0.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.0.0
## [7.0.0-rc.3] - October 19, 2021
This is the last release candidate for Rojo 7. In an effort to get Rojo 7 out the door, we'll be freezing features from here on out, something we should've done a couple months ago.
Expect to see Rojo 7 stable soon!
* Added support for writing `Tags` in project files, model files, and meta files. ([#484])
* Adjusted Studio plugin colors to match Roblox Studio palette. ([#482])
* Improved experimental two-way sync feature by batching changes. ([#478])
[#482]: https://github.com/rojo-rbx/rojo/pull/482
[#484]: https://github.com/rojo-rbx/rojo/pull/484
[#478]: https://github.com/rojo-rbx/rojo/pull/478
[7.0.0-rc.3]: https://github.com/rojo-rbx/rojo/releases/tag/v7.0.0-rc.3
## 7.0.0-rc.2 - October 19, 2021
(Botched release due to Git mishap, oops!)
## [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])
* Fixed syncing refs in the Roblox Studio plugin ([#462], [#466])
* Added support for long paths on Windows. ([#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) ## [7.0.0-alpha.4][7.0.0-alpha.4] (May 5, 2021)
* Added the `gameId` and `placeId` optional properties to project files. * 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. * When connecting from the Rojo Roblox Studio plugin, Rojo will set the game and place ID of the current place to these values, if set.
* This is equivalent to running `game:SetUniverseId(...)` and `game:SetPlaceId(...)` from the command bar in Studio. * This is equivalent to running `game:SetUniverseId(...)` and `game:SetPlaceId(...)` from the command bar in Studio.
* Added "EXPERIMENTAL!" label to two-way sync toggle in Rojo's Roblox Studio plugin. * Added "EXPERIMENTAL!" label to two-way sync toggle in Rojo's Roblox Studio plugin.
* Fixed `Name` and `Parent` properties being allowed in Rojo projects. ([#413][pr-413]) * Fixed `Name` and `Parent` properties being allowed in Rojo projects. ([#413][pr-413])
* Fixed "Open Scripts Externally" feature crashing Studio. ([#369][issue-369]) * Fixed "Open Scripts Externally" feature crashing Studio. ([#369][issue-369])

View File

@@ -29,25 +29,29 @@ Sometimes there's something that Rojo doesn't do that it probably should.
Please file issues and we'll try to help figure out what the best way forward is. Please file issues and we'll try to help figure out what the best way forward is.
## Local Development Gotchas
If your build fails with "Error: failed to open file `D:\code\rojo\plugin\modules\roact\src`" you need to update your Git submodules.
Run the command and try building again: `git submodule update --init --recursive`.
## Pushing a Rojo Release ## Pushing a Rojo Release
The Rojo release process is pretty manual right now. If you need to do it, here's how: The Rojo release process is pretty manual right now. If you need to do it, here's how:
1. Bump server version in [`Cargo.toml`](Cargo.toml) 1. Bump server version in [`Cargo.toml`](Cargo.toml)
2. Bump plugin version in [`plugin/src/Config.lua`](plugin/src/Config.lua) 2. Bump plugin version in [`plugin/src/Config.lua`](plugin/src/Config.lua)
3. Run `cargo test` to update `Cargo.lock` and double-check tests 3. Run `cargo test` to update `Cargo.lock` and run tests
4. Update [`CHANGELOG.md`](CHANGELOG.md) 4. Update [`CHANGELOG.md`](CHANGELOG.md)
5. Commit! 5. Commit!
* `git add . && git commit -m "Release vX.Y.Z"` * `git add . && git commit -m "Release vX.Y.Z"`
6. Tag the commit with the version from `Cargo.toml` prepended with a v, like `v0.4.13` 6. Tag the commit
* `git tag vX.Y.Z`
7. Publish the CLI 7. Publish the CLI
* `cargo publish` * `cargo publish`
8. Publish the Plugin 8. Publish the Plugin
* `rojo publish plugin --asset_id 6415005344` * `cargo run -- upload plugin --asset_id 6415005344`
* `rojo build plugin -o Rojo.rbxm`
9. Push commits and tags 9. Push commits and tags
* `git push && git push --tags` * `git push && git push --tags`
10. Copy GitHub release content from previous release 10. Copy GitHub release content from previous release
* Update the leading text with a summary about the release * Update the leading text with a summary about the release
* Paste the changelog notes (as-is!) from [`CHANGELOG.md`](CHANGELOG.md) * Paste the changelog notes (as-is!) from [`CHANGELOG.md`](CHANGELOG.md)
* Write a small summary of each major feature * Write a small summary of each major feature
* Attach release artifacts from GitHub Actions for each platform

1873
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rojo" name = "rojo"
version = "7.0.0-alpha.4" version = "7.1.1"
authors = ["Lucien Greathouse <me@lpghatguy.com>"] authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "Enables professional-grade development tools for Roblox developers" description = "Enables professional-grade development tools for Roblox developers"
license = "MPL-2.0" license = "MPL-2.0"
@@ -9,6 +9,7 @@ documentation = "https://rojo.space/docs"
repository = "https://github.com/rojo-rbx/rojo" repository = "https://github.com/rojo-rbx/rojo"
readme = "README.md" readme = "README.md"
edition = "2018" edition = "2018"
build = "build.rs"
exclude = [ exclude = [
"/test-projects/**", "/test-projects/**",
@@ -36,16 +37,12 @@ members = [
name = "librojo" name = "librojo"
path = "src/lib.rs" path = "src/lib.rs"
[[bin]]
name = "rojo"
path = "src/bin.rs"
[[bench]] [[bench]]
name = "build" name = "build"
harness = false harness = false
[dependencies] [dependencies]
memofs = { version = "0.1.2", path = "memofs" } memofs = { version = "0.2.0", path = "memofs" }
# These dependencies can be uncommented when working on rbx-dom simultaneously # These dependencies can be uncommented when working on rbx-dom simultaneously
# rbx_binary = { path = "../rbx-dom/rbx_binary" } # rbx_binary = { path = "../rbx-dom/rbx_binary" }
@@ -54,61 +51,58 @@ memofs = { version = "0.1.2", path = "memofs" }
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" } # rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
# rbx_xml = { path = "../rbx-dom/rbx_xml" } # rbx_xml = { path = "../rbx-dom/rbx_xml" }
rbx_binary = "0.6.0-alpha.1" rbx_binary = "0.6.4"
rbx_dom_weak = "2.0.0-alpha.1" rbx_dom_weak = "2.3.0"
rbx_reflection = "4.0.0-alpha.1" rbx_reflection = "4.2.0"
rbx_reflection_database = "0.1.0" rbx_reflection_database = "0.2.2"
rbx_xml = "0.12.0-alpha.1" rbx_xml = "0.12.3"
anyhow = "1.0.27" anyhow = "1.0.44"
backtrace = "0.3" backtrace = "0.3.61"
bincode = "1.2.1" bincode = "1.3.3"
crossbeam-channel = "0.4.0" crossbeam-channel = "0.5.1"
csv = "1.1.1" csv = "1.1.6"
env_logger = "0.7.1" env_logger = "0.9.0"
fs-err = "2.2.0" fs-err = "2.6.0"
futures = "0.1.29" futures = "0.3.17"
globset = "0.4.4" globset = "0.4.8"
humantime = "1.3.0" humantime = "2.1.0"
hyper = "0.12.35" hyper = { version = "0.14.13", features = ["server", "tcp", "http1"] }
jod-thread = "0.1.0" jod-thread = "0.1.2"
lazy_static = "1.4.0" log = "0.4.14"
log = "0.4.8" maplit = "1.0.2"
maplit = "1.0.1" notify = "4.0.17"
notify = "4.0.14" opener = "0.5.0"
opener = "0.4.1" reqwest = { version = "0.11.10", features = ["blocking", "json"] }
regex = "1.3.1"
reqwest = "0.9.20"
ritz = "0.1.0" ritz = "0.1.0"
rlua = "0.17.0" roblox_install = "1.0.0"
roblox_install = "0.2.2" serde = { version = "1.0.130", features = ["derive", "rc"] }
serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0.68"
serde_json = "1.0" structopt = "0.3.23"
structopt = "0.3.5" termcolor = "1.1.2"
termcolor = "1.0.5" thiserror = "1.0.30"
thiserror = "1.0.11" tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
tokio = "0.1.22" uuid = { version = "1.0.0", features = ["v4", "serde"] }
uuid = { version = "0.8.1", features = ["v4", "serde"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winreg = "0.6.2" winreg = "0.10.1"
[build-dependencies] [build-dependencies]
memofs = { version = "0.1.3", path = "memofs" } memofs = { version = "0.2.0", path = "memofs" }
anyhow = "1.0.27" embed-resource = "1.6.4"
bincode = "1.2.1" anyhow = "1.0.44"
fs-err = "2.3.0" bincode = "1.3.3"
maplit = "1.0.1" fs-err = "2.6.0"
maplit = "1.0.2"
[dev-dependencies] [dev-dependencies]
rojo-insta-ext = { path = "rojo-insta-ext" } rojo-insta-ext = { path = "rojo-insta-ext" }
criterion = "0.3" criterion = "0.3.5"
insta = { version = "1.3.0", features = ["redactions"] } insta = { version = "1.8.0", features = ["redactions"] }
lazy_static = "1.2" paste = "1.0.5"
paste = "0.1" pretty_assertions = "1.2.1"
pretty_assertions = "0.6.1" serde_yaml = "0.8.21"
serde_yaml = "0.8.9" tempfile = "3.2.0"
tempfile = "3.0" walkdir = "2.3.2"
walkdir = "2.1"

View File

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

BIN
assets/icon-link-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
assets/icon-warn-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

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 ## Unreleased Changes
## 0.2.0 (2021-08-23)
* Updated to `crossbeam-channel` 0.5.1.
## 0.1.3 (2020-11-19) ## 0.1.3 (2020-11-19)
* Added `set_watch_enabled` to `Vfs` and `VfsLock` to allow turning off file watching. * Added `set_watch_enabled` to `Vfs` and `VfsLock` to allow turning off file watching.

View File

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

View File

@@ -71,8 +71,8 @@ types = {
CFrame = { CFrame = {
fromPod = function(pod) fromPod = function(pod)
local pos = pod.Position local pos = pod.position
local orient = pod.Orientation local orient = pod.orientation
return CFrame.new( return CFrame.new(
pos[1], pos[2], pos[3], pos[1], pos[2], pos[3],
@@ -89,8 +89,8 @@ types = {
r20, r21, r22 = roblox:GetComponents() r20, r21, r22 = roblox:GetComponents()
return { return {
Position = {x, y, z}, position = {x, y, z},
Orientation = { orientation = {
{r00, r01, r02}, {r00, r01, r02},
{r10, r11, r12}, {r10, r11, r12},
{r20, r21, r22}, {r20, r21, r22},
@@ -123,10 +123,10 @@ types = {
fromPod = function(pod) fromPod = function(pod)
local keypoints = {} local keypoints = {}
for index, keypoint in ipairs(pod.Keypoints) do for index, keypoint in ipairs(pod.keypoints) do
keypoints[index] = ColorSequenceKeypoint.new( keypoints[index] = ColorSequenceKeypoint.new(
keypoint.Time, keypoint.time,
types.Color3.fromPod(keypoint.Color) types.Color3.fromPod(keypoint.color)
) )
end end
@@ -138,13 +138,13 @@ types = {
for index, keypoint in ipairs(roblox.Keypoints) do for index, keypoint in ipairs(roblox.Keypoints) do
keypoints[index] = { keypoints[index] = {
Time = keypoint.Time, time = keypoint.Time,
Color = types.Color3.toPod(keypoint.Value), color = types.Color3.toPod(keypoint.Value),
} }
end end
return { return {
Keypoints = keypoints, keypoints = keypoints,
} }
end, end,
}, },
@@ -223,11 +223,11 @@ types = {
fromPod = function(pod) fromPod = function(pod)
local keypoints = {} local keypoints = {}
for index, keypoint in ipairs(pod.Keypoints) do for index, keypoint in ipairs(pod.keypoints) do
keypoints[index] = NumberSequenceKeypoint.new( keypoints[index] = NumberSequenceKeypoint.new(
keypoint.Time, keypoint.time,
keypoint.Value, keypoint.value,
keypoint.Envelope keypoint.envelope
) )
end end
@@ -239,14 +239,14 @@ types = {
for index, keypoint in ipairs(roblox.Keypoints) do for index, keypoint in ipairs(roblox.Keypoints) do
keypoints[index] = { keypoints[index] = {
Time = keypoint.Time, time = keypoint.Time,
Value = keypoint.Value, value = keypoint.Value,
Envelope = keypoint.Envelope, envelope = keypoint.Envelope,
} }
end end
return { return {
Keypoints = keypoints, keypoints = keypoints,
} }
end, end,
}, },
@@ -257,11 +257,11 @@ types = {
return nil return nil
else else
return PhysicalProperties.new( return PhysicalProperties.new(
pod.Density, pod.density,
pod.Friction, pod.friction,
pod.Elasticity, pod.elasticity,
pod.FrictionWeight, pod.frictionWeight,
pod.ElasticityWeight pod.elasticityWeight
) )
end end
end, end,
@@ -271,11 +271,11 @@ types = {
return "Default" return "Default"
else else
return { return {
Density = roblox.Density, density = roblox.Density,
Friction = roblox.Friction, friction = roblox.Friction,
Elasticity = roblox.Elasticity, elasticity = roblox.Elasticity,
FrictionWeight = roblox.FrictionWeight, frictionWeight = roblox.FrictionWeight,
ElasticityWeight = roblox.ElasticityWeight, elasticityWeight = roblox.ElasticityWeight,
} }
end end
end, end,
@@ -284,15 +284,15 @@ types = {
Ray = { Ray = {
fromPod = function(pod) fromPod = function(pod)
return Ray.new( return Ray.new(
types.Vector3.fromPod(pod.Origin), types.Vector3.fromPod(pod.origin),
types.Vector3.fromPod(pod.Direction) types.Vector3.fromPod(pod.direction)
) )
end, end,
toPod = function(roblox) toPod = function(roblox)
return { return {
Origin = types.Vector3.toPod(roblox.Origin), origin = types.Vector3.toPod(roblox.Origin),
Direction = types.Vector3.toPod(roblox.Direction), direction = types.Vector3.toPod(roblox.Direction),
} }
end, end,
}, },
@@ -388,6 +388,11 @@ types = {
end, end,
}, },
Tags = {
fromPod = identity,
toPod = identity,
},
Vector2 = { Vector2 = {
fromPod = unpackDecoder(Vector2.new), fromPod = unpackDecoder(Vector2.new),
@@ -431,12 +436,14 @@ types = {
local EncodedValue = {} local EncodedValue = {}
function EncodedValue.decode(encodedValue) function EncodedValue.decode(encodedValue)
local typeImpl = types[encodedValue.Type] local ty, value = next(encodedValue)
local typeImpl = types[ty]
if typeImpl == nil then if typeImpl == nil then
return false, "Couldn't decode value " .. tostring(encodedValue.Type) return false, "Couldn't decode value " .. tostring(ty)
end end
return true, typeImpl.fromPod(encodedValue.Value) return true, typeImpl.fromPod(value)
end end
function EncodedValue.encode(rbxValue, propertyType) function EncodedValue.encode(rbxValue, propertyType)
@@ -448,8 +455,7 @@ function EncodedValue.encode(rbxValue, propertyType)
end end
return true, { return true, {
Type = propertyType, [propertyType] = typeImpl.toPod(rbxValue),
Value = typeImpl.toPod(rbxValue),
} }
end end

View File

@@ -20,7 +20,21 @@ local function set(container, key, value)
end end
function PropertyDescriptor.fromRaw(data, className, propertyName) function PropertyDescriptor.fromRaw(data, className, propertyName)
local key, value = next(data.DataType)
return setmetatable({ return setmetatable({
-- The meanings of the key and value in DataType differ when the type of
-- the property is Enum. When the property is of type Enum, the key is
-- the name of the type:
--
-- { Enum = "<name of enum>" }
--
-- When the property is not of type Enum, the value is the name of the
-- type:
--
-- { Value = "<data type>" }
dataType = key == "Enum" and key or value,
scriptability = data.Scriptability, scriptability = data.Scriptability,
className = className, className = className,
name = propertyName, name = propertyName,
@@ -77,4 +91,4 @@ function PropertyDescriptor:write(instance, value)
end end
end end
return PropertyDescriptor return PropertyDescriptor

View File

@@ -1,8 +1,7 @@
{ {
"Axes": { "Axes": {
"value": { "value": {
"Type": "Axes", "Axes": [
"Value": [
"X", "X",
"Y", "Y",
"Z" "Z"
@@ -12,35 +11,31 @@
}, },
"BinaryString": { "BinaryString": {
"value": { "value": {
"Type": "BinaryString", "BinaryString": "SGVsbG8h"
"Value": "SGVsbG8h"
}, },
"ty": "BinaryString" "ty": "BinaryString"
}, },
"Bool": { "Bool": {
"value": { "value": {
"Type": "Bool", "Bool": true
"Value": true
}, },
"ty": "Bool" "ty": "Bool"
}, },
"BrickColor": { "BrickColor": {
"value": { "value": {
"Type": "BrickColor", "BrickColor": 1004
"Value": 1004
}, },
"ty": "BrickColor" "ty": "BrickColor"
}, },
"CFrame": { "CFrame": {
"value": { "value": {
"Type": "CFrame", "CFrame": {
"Value": { "position": [
"Position": [
1.0, 1.0,
2.0, 2.0,
3.0 3.0
], ],
"Orientation": [ "orientation": [
[ [
4.0, 4.0,
5.0, 5.0,
@@ -63,8 +58,7 @@
}, },
"Color3": { "Color3": {
"value": { "value": {
"Type": "Color3", "Color3": [
"Value": [
1.0, 1.0,
2.0, 2.0,
3.0 3.0
@@ -74,8 +68,7 @@
}, },
"Color3uint8": { "Color3uint8": {
"value": { "value": {
"Type": "Color3uint8", "Color3uint8": [
"Value": [
0, 0,
128, 128,
255 255
@@ -85,20 +78,19 @@
}, },
"ColorSequence": { "ColorSequence": {
"value": { "value": {
"Type": "ColorSequence", "ColorSequence": {
"Value": { "keypoints": [
"Keypoints": [
{ {
"Time": 0.0, "time": 0.0,
"Color": [ "color": [
1.0, 1.0,
1.0, 1.0,
0.5 0.5
] ]
}, },
{ {
"Time": 1.0, "time": 1.0,
"Color": [ "color": [
0.0, 0.0,
0.0, 0.0,
0.0 0.0
@@ -111,22 +103,19 @@
}, },
"Content": { "Content": {
"value": { "value": {
"Type": "Content", "Content": "rbxassetid://12345"
"Value": "rbxassetid://12345"
}, },
"ty": "Content" "ty": "Content"
}, },
"Enum": { "Enum": {
"value": { "value": {
"Type": "Enum", "Enum": 1234
"Value": 1234
}, },
"ty": "Enum" "ty": "Enum"
}, },
"Faces": { "Faces": {
"value": { "value": {
"Type": "Faces", "Faces": [
"Value": [
"Right", "Right",
"Top", "Top",
"Back", "Back",
@@ -139,36 +128,31 @@
}, },
"Float32": { "Float32": {
"value": { "value": {
"Type": "Float32", "Float32": 15.0
"Value": 15.0
}, },
"ty": "Float32" "ty": "Float32"
}, },
"Float64": { "Float64": {
"value": { "value": {
"Type": "Float64", "Float64": 15123.0
"Value": 15123.0
}, },
"ty": "Float64" "ty": "Float64"
}, },
"Int32": { "Int32": {
"value": { "value": {
"Type": "Int32", "Int32": 6014
"Value": 6014
}, },
"ty": "Int32" "ty": "Int32"
}, },
"Int64": { "Int64": {
"value": { "value": {
"Type": "Int64", "Int64": 23491023
"Value": 23491023
}, },
"ty": "Int64" "ty": "Int64"
}, },
"NumberRange": { "NumberRange": {
"value": { "value": {
"Type": "NumberRange", "NumberRange": [
"Value": [
-36.0, -36.0,
94.0 94.0
] ]
@@ -177,18 +161,17 @@
}, },
"NumberSequence": { "NumberSequence": {
"value": { "value": {
"Type": "NumberSequence", "NumberSequence": {
"Value": { "keypoints": [
"Keypoints": [
{ {
"Time": 0.0, "time": 0.0,
"Value": 5.0, "value": 5.0,
"Envelope": 2.0 "envelope": 2.0
}, },
{ {
"Time": 1.0, "time": 1.0,
"Value": 22.0, "value": 22.0,
"Envelope": 0.0 "envelope": 0.0
} }
] ]
} }
@@ -197,34 +180,31 @@
}, },
"PhysicalProperties-Custom": { "PhysicalProperties-Custom": {
"value": { "value": {
"Type": "PhysicalProperties", "PhysicalProperties": {
"Value": { "density": 0.5,
"Density": 0.5, "friction": 1.0,
"Friction": 1.0, "elasticity": 0.0,
"Elasticity": 0.0, "frictionWeight": 50.0,
"FrictionWeight": 50.0, "elasticityWeight": 25.0
"ElasticityWeight": 25.0
} }
}, },
"ty": "PhysicalProperties" "ty": "PhysicalProperties"
}, },
"PhysicalProperties-Default": { "PhysicalProperties-Default": {
"value": { "value": {
"Type": "PhysicalProperties", "PhysicalProperties": "Default"
"Value": "Default"
}, },
"ty": "PhysicalProperties" "ty": "PhysicalProperties"
}, },
"Ray": { "Ray": {
"value": { "value": {
"Type": "Ray", "Ray": {
"Value": { "origin": [
"Origin": [
1.0, 1.0,
2.0, 2.0,
3.0 3.0
], ],
"Direction": [ "direction": [
4.0, 4.0,
5.0, 5.0,
6.0 6.0
@@ -235,8 +215,7 @@
}, },
"Rect": { "Rect": {
"value": { "value": {
"Type": "Rect", "Rect": [
"Value": [
[ [
0.0, 0.0,
5.0 5.0
@@ -251,8 +230,7 @@
}, },
"Region3int16": { "Region3int16": {
"value": { "value": {
"Type": "Region3int16", "Region3int16": [
"Value": [
[ [
-10, -10,
-5, -5,
@@ -269,15 +247,23 @@
}, },
"String": { "String": {
"value": { "value": {
"Type": "String", "String": "Hello, world!"
"Value": "Hello, world!"
}, },
"ty": "String" "ty": "String"
}, },
"Tags": {
"value": {
"Tags": [
"foo",
"con'fusion?!",
"bar"
]
},
"ty": "Tags"
},
"UDim": { "UDim": {
"value": { "value": {
"Type": "UDim", "UDim": [
"Value": [
1.0, 1.0,
32 32
] ]
@@ -286,8 +272,7 @@
}, },
"UDim2": { "UDim2": {
"value": { "value": {
"Type": "UDim2", "UDim2": [
"Value": [
[ [
-1.0, -1.0,
100 100
@@ -302,8 +287,7 @@
}, },
"Vector2": { "Vector2": {
"value": { "value": {
"Type": "Vector2", "Vector2": [
"Value": [
-50.0, -50.0,
50.0 50.0
] ]
@@ -312,8 +296,7 @@
}, },
"Vector2int16": { "Vector2int16": {
"value": { "value": {
"Type": "Vector2int16", "Vector2int16": [
"Value": [
-300, -300,
300 300
] ]
@@ -322,8 +305,7 @@
}, },
"Vector3": { "Vector3": {
"value": { "value": {
"Type": "Vector3", "Vector3": [
"Value": [
-300.0, -300.0,
0.0, 0.0,
1500.0 1500.0
@@ -333,8 +315,7 @@
}, },
"Vector3int16": { "Vector3int16": {
"value": { "value": {
"Type": "Vector3int16", "Vector3int16": [
"Value": [
60, 60,
37, 37,
-450 -450

View File

@@ -6,12 +6,10 @@ local CollectionService = game:GetService("CollectionService")
return { return {
Instance = { Instance = {
Tags = { Tags = {
read = function(instance, key) read = function(instance)
local tagList = CollectionService:GetTags(instance) return true, CollectionService:GetTags(instance)
return true, table.concat(tagList, "\0")
end, end,
write = function(instance, key, value) write = function(instance, _, value)
local existingTags = CollectionService:GetTags(instance) local existingTags = CollectionService:GetTags(instance)
local unseenTags = {} local unseenTags = {}
@@ -19,8 +17,7 @@ return {
unseenTags[tag] = true unseenTags[tag] = true
end end
local tagList = string.split(value, "\0") for _, tag in ipairs(value) do
for _, tag in ipairs(tagList) do
unseenTags[tag] = nil unseenTags[tag] = nil
CollectionService:AddTag(instance, tag) CollectionService:AddTag(instance, tag)
end end
@@ -44,4 +41,4 @@ return {
end, end,
}, },
}, },
} }

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 local aliasData = propertyData.Kind.Alias
if aliasData ~= nil then if aliasData ~= nil then
return PropertyDescriptor.fromRaw( return PropertyDescriptor.fromRaw(
currentClass.properties[aliasData.AliasFor], currentClass.Properties[aliasData.AliasFor],
currentClassName, currentClassName,
aliasData.AliasFor) aliasData.AliasFor)
end end
@@ -66,4 +66,4 @@ return {
findCanonicalPropertyDescriptor = findCanonicalPropertyDescriptor, findCanonicalPropertyDescriptor = findCanonicalPropertyDescriptor,
Error = Error, Error = Error,
EncodedValue = require(script.EncodedValue), EncodedValue = require(script.EncodedValue),
} }

View File

@@ -0,0 +1,40 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Dictionary = require(Plugin.Dictionary)
local StudioPluginContext = require(script.Parent.StudioPluginContext)
local e = Roact.createElement
local StudioPluginAction = Roact.Component:extend("StudioPluginAction")
function StudioPluginAction:init()
self.pluginAction = self.props.plugin:CreatePluginAction(
self.props.name, self.props.title, self.props.description, self.props.icon, self.props.bindable
)
self.pluginAction.Triggered:Connect(self.props.onTriggered)
end
function StudioPluginAction:render()
return nil
end
function StudioPluginAction:willUnmount()
self.pluginAction:Destroy()
end
local function StudioPluginActionWrapper(props)
return e(StudioPluginContext.Consumer, {
render = function(plugin)
return e(StudioPluginAction, Dictionary.merge(props, {
plugin = plugin,
}))
end,
})
end
return StudioPluginActionWrapper

View File

@@ -44,6 +44,10 @@ function StudioToggleButton:didUpdate(lastProps)
self.button.Enabled = self.props.enabled self.button.Enabled = self.props.enabled
end end
if self.props.icon ~= lastProps.icon then
self.button.Icon = self.props.icon
end
if self.props.active ~= lastProps.active then if self.props.active ~= lastProps.active then
self.button:SetActive(self.props.active) self.button:SetActive(self.props.active)
end end
@@ -63,4 +67,4 @@ local function StudioToggleButtonWrapper(props)
}) })
end end
return StudioToggleButtonWrapper return StudioToggleButtonWrapper

View File

@@ -24,7 +24,7 @@ local function AddressEntry(props)
layoutOrder = props.layoutOrder, layoutOrder = props.layoutOrder,
}, { }, {
Host = e("TextBox", { Host = e("TextBox", {
Text = "", Text = props.host or "",
Font = Enum.Font.Code, Font = Enum.Font.Code,
TextSize = 18, TextSize = 18,
TextColor3 = theme.AddressEntry.TextColor, TextColor3 = theme.AddressEntry.TextColor,
@@ -32,6 +32,7 @@ local function AddressEntry(props)
TextTransparency = props.transparency, TextTransparency = props.transparency,
PlaceholderText = Config.defaultHost, PlaceholderText = Config.defaultHost,
PlaceholderColor3 = theme.AddressEntry.PlaceholderColor, PlaceholderColor3 = theme.AddressEntry.PlaceholderColor,
ClearTextOnFocus = false,
Size = UDim2.new(1, -(HOST_OFFSET + DIVIDER_WIDTH + PORT_WIDTH), 1, 0), Size = UDim2.new(1, -(HOST_OFFSET + DIVIDER_WIDTH + PORT_WIDTH), 1, 0),
Position = UDim2.new(0, HOST_OFFSET, 0, 0), Position = UDim2.new(0, HOST_OFFSET, 0, 0),
@@ -39,17 +40,22 @@ local function AddressEntry(props)
ClipsDescendants = true, ClipsDescendants = true,
BackgroundTransparency = 1, BackgroundTransparency = 1,
[Roact.Ref] = props.hostRef, [Roact.Change.Text] = function(object)
if props.onHostChange ~= nil then
props.onHostChange(object.Text)
end
end
}), }),
Port = e("TextBox", { Port = e("TextBox", {
Text = "", Text = props.port or "",
Font = Enum.Font.Code, Font = Enum.Font.Code,
TextSize = 18, TextSize = 18,
TextColor3 = theme.AddressEntry.TextColor, TextColor3 = theme.AddressEntry.TextColor,
TextTransparency = props.transparency, TextTransparency = props.transparency,
PlaceholderText = Config.defaultPort, PlaceholderText = Config.defaultPort,
PlaceholderColor3 = theme.AddressEntry.PlaceholderColor, PlaceholderColor3 = theme.AddressEntry.PlaceholderColor,
ClearTextOnFocus = false,
Size = UDim2.new(0, PORT_WIDTH, 1, 0), Size = UDim2.new(0, PORT_WIDTH, 1, 0),
Position = UDim2.new(1, 0, 0, 0), Position = UDim2.new(1, 0, 0, 0),
@@ -58,12 +64,14 @@ local function AddressEntry(props)
ClipsDescendants = true, ClipsDescendants = true,
BackgroundTransparency = 1, BackgroundTransparency = 1,
[Roact.Ref] = props.portRef,
[Roact.Change.Text] = function(object) [Roact.Change.Text] = function(object)
local text = object.Text local text = object.Text
text = text:gsub("%D", "") text = text:gsub("%D", "")
object.Text = text object.Text = text
if props.onPortChange ~= nil then
props.onPortChange(text)
end
end, end,
}, { }, {
Divider = e("Frame", { Divider = e("Frame", {
@@ -80,11 +88,6 @@ end
local NotConnectedPage = Roact.Component:extend("NotConnectedPage") local NotConnectedPage = Roact.Component:extend("NotConnectedPage")
function NotConnectedPage:init()
self.hostRef = Roact.createRef()
self.portRef = Roact.createRef()
end
function NotConnectedPage:render() function NotConnectedPage:render()
return Roact.createFragment({ return Roact.createFragment({
Header = e(Header, { Header = e(Header, {
@@ -93,8 +96,10 @@ function NotConnectedPage:render()
}), }),
AddressEntry = e(AddressEntry, { AddressEntry = e(AddressEntry, {
hostRef = self.hostRef, host = self.props.host,
portRef = self.portRef, port = self.props.port,
onHostChange = self.props.onHostChange,
onPortChange = self.props.onPortChange,
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 2, layoutOrder = 2,
}), }),
@@ -117,15 +122,7 @@ function NotConnectedPage:render()
style = "Solid", style = "Solid",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 2, layoutOrder = 2,
onClick = function() onClick = self.props.onConnect,
local hostText = self.hostRef.current.Text
local portText = self.portRef.current.Text
self.props.onConnect(
#hostText > 0 and hostText or Config.defaultHost,
#portText > 0 and portText or Config.defaultPort
)
end,
}), }),
Layout = e("UIListLayout", { Layout = e("UIListLayout", {

View File

@@ -34,7 +34,7 @@ end
local BRAND_COLOR = hexColor(0xE13835) local BRAND_COLOR = hexColor(0xE13835)
local lightTheme = strict("LightTheme", { local lightTheme = strict("LightTheme", {
BackgroundColor = hexColor(0xF0F0F0), BackgroundColor = hexColor(0xFFFFFF),
Button = { Button = {
Solid = { Solid = {
ActionFillColor = hexColor(0xFFFFFF), ActionFillColor = hexColor(0xFFFFFF),
@@ -67,7 +67,7 @@ local lightTheme = strict("LightTheme", {
BackgroundColor = BRAND_COLOR, BackgroundColor = BRAND_COLOR,
}, },
Inactive = { Inactive = {
IconColor = hexColor(0xCACACA), IconColor = hexColor(0xEEEEEE),
BorderColor = hexColor(0xAFAFAF), BorderColor = hexColor(0xAFAFAF),
}, },
}, },
@@ -77,11 +77,11 @@ local lightTheme = strict("LightTheme", {
}, },
BorderedContainer = { BorderedContainer = {
BorderColor = hexColor(0xCBCBCB), BorderColor = hexColor(0xCBCBCB),
BackgroundColor = hexColor(0xE0E0E0), BackgroundColor = hexColor(0xEEEEEE),
}, },
Spinner = { Spinner = {
ForegroundColor = BRAND_COLOR, ForegroundColor = BRAND_COLOR,
BackgroundColor = hexColor(0xE0E0E0), BackgroundColor = hexColor(0xEEEEEE),
}, },
ConnectionDetails = { ConnectionDetails = {
ProjectNameColor = hexColor(0x00000), ProjectNameColor = hexColor(0x00000),
@@ -108,7 +108,7 @@ local lightTheme = strict("LightTheme", {
}) })
local darkTheme = strict("DarkTheme", { local darkTheme = strict("DarkTheme", {
BackgroundColor = hexColor(0x272727), BackgroundColor = hexColor(0x2E2E2E),
Button = { Button = {
Solid = { Solid = {
ActionFillColor = hexColor(0xFFFFFF), ActionFillColor = hexColor(0xFFFFFF),
@@ -147,15 +147,15 @@ local darkTheme = strict("DarkTheme", {
}, },
AddressEntry = { AddressEntry = {
TextColor = hexColor(0xFFFFFF), TextColor = hexColor(0xFFFFFF),
PlaceholderColor = hexColor(0x717171) PlaceholderColor = hexColor(0x8B8B8B)
}, },
BorderedContainer = { BorderedContainer = {
BorderColor = hexColor(0x535353), BorderColor = hexColor(0x535353),
BackgroundColor = hexColor(0x323232), BackgroundColor = hexColor(0x2B2B2B),
}, },
Spinner = { Spinner = {
ForegroundColor = BRAND_COLOR, ForegroundColor = BRAND_COLOR,
BackgroundColor = hexColor(0x323232), BackgroundColor = hexColor(0x2B2B2B),
}, },
ConnectionDetails = { ConnectionDetails = {
ProjectNameColor = hexColor(0xFFFFFF), ProjectNameColor = hexColor(0xFFFFFF),
@@ -236,4 +236,4 @@ return {
StudioProvider = StudioProvider, StudioProvider = StudioProvider,
Consumer = Context.Consumer, Consumer = Context.Consumer,
with = with, with = with,
} }

View File

@@ -16,6 +16,7 @@ local Theme = require(script.Theme)
local PluginSettings = require(script.PluginSettings) local PluginSettings = require(script.PluginSettings)
local Page = require(script.Page) local Page = require(script.Page)
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
local StudioToolbar = require(script.Components.Studio.StudioToolbar) local StudioToolbar = require(script.Components.Studio.StudioToolbar)
local StudioToggleButton = require(script.Components.Studio.StudioToggleButton) local StudioToggleButton = require(script.Components.Studio.StudioToggleButton)
local StudioPluginGui = require(script.Components.Studio.StudioPluginGui) local StudioPluginGui = require(script.Components.Studio.StudioPluginGui)
@@ -37,13 +38,34 @@ local App = Roact.Component:extend("App")
function App:init() function App:init()
preloadAssets() preloadAssets()
self.host, self.setHost = Roact.createBinding("")
self.port, self.setPort = Roact.createBinding("")
self:setState({ self:setState({
appStatus = AppStatus.NotConnected, appStatus = AppStatus.NotConnected,
guiEnabled = false, guiEnabled = false,
toolbarIcon = Assets.Images.PluginButton,
}) })
end end
function App:startSession(host, port, sessionOptions) function App:getHostAndPort()
local host = self.host:getValue()
local port = self.port:getValue()
local host = if #host > 0 then host else Config.defaultHost
local port = if #port > 0 then port else Config.defaultPort
return host, port
end
function App:startSession()
local host, port = self:getHostAndPort()
local sessionOptions = {
openScriptsExternally = self.props.settings:get("openScriptsExternally"),
twoWaySync = self.props.settings:get("twoWaySync"),
}
local baseUrl = ("http://%s:%s"):format(host, port) local baseUrl = ("http://%s:%s"):format(host, port)
local apiContext = ApiContext.new(baseUrl) local apiContext = ApiContext.new(baseUrl)
@@ -57,6 +79,7 @@ function App:startSession(host, port, sessionOptions)
if status == ServeSession.Status.Connecting then if status == ServeSession.Status.Connecting then
self:setState({ self:setState({
appStatus = AppStatus.Connecting, appStatus = AppStatus.Connecting,
toolbarIcon = Assets.Images.PluginButton,
}) })
elseif status == ServeSession.Status.Connected then elseif status == ServeSession.Status.Connected then
local address = ("%s:%s"):format(host, port) local address = ("%s:%s"):format(host, port)
@@ -64,7 +87,10 @@ function App:startSession(host, port, sessionOptions)
appStatus = AppStatus.Connected, appStatus = AppStatus.Connected,
projectName = details, projectName = details,
address = address, address = address,
toolbarIcon = Assets.Images.PluginButtonConnected,
}) })
Log.info("Connected to session '{}' at {}", details, address)
elseif status == ServeSession.Status.Disconnected then elseif status == ServeSession.Status.Disconnected then
self.serveSession = nil self.serveSession = nil
@@ -76,11 +102,15 @@ function App:startSession(host, port, sessionOptions)
self:setState({ self:setState({
appStatus = AppStatus.Error, appStatus = AppStatus.Error,
errorMessage = tostring(details), errorMessage = tostring(details),
toolbarIcon = Assets.Images.PluginButtonWarning,
}) })
else else
self:setState({ self:setState({
appStatus = AppStatus.NotConnected, appStatus = AppStatus.NotConnected,
toolbarIcon = Assets.Images.PluginButton,
}) })
Log.info("Disconnected session")
end end
end end
end) end)
@@ -90,6 +120,22 @@ function App:startSession(host, port, sessionOptions)
self.serveSession = serveSession self.serveSession = serveSession
end end
function App:endSession()
if self.serveSession == nil then
return
end
Log.trace("Disconnecting session")
self.serveSession:stop()
self.serveSession = nil
self:setState({
appStatus = AppStatus.NotConnected,
})
Log.trace("Session terminated by user")
end
function App:render() function App:render()
local pluginName = "Rojo " .. Version.display(Config.version) local pluginName = "Rojo " .. Version.display(Config.version)
@@ -108,119 +154,160 @@ function App:render()
value = self.props.plugin, value = self.props.plugin,
}, { }, {
e(Theme.StudioProvider, nil, { e(Theme.StudioProvider, nil, {
e(PluginSettings.StudioProvider, { gui = e(StudioPluginGui, {
plugin = self.props.plugin, id = pluginName,
title = pluginName,
active = self.state.guiEnabled,
initDockState = Enum.InitialDockState.Right,
initEnabled = false,
overridePreviousState = false,
floatingSize = Vector2.new(300, 200),
minimumSize = Vector2.new(300, 200),
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
onInitialState = function(initialState)
self:setState({
guiEnabled = initialState,
})
end,
onClose = function()
self:setState({
guiEnabled = false,
})
end,
}, { }, {
gui = e(StudioPluginGui, { NotConnectedPage = createPageElement(AppStatus.NotConnected, {
id = pluginName, host = self.host,
title = pluginName, onHostChange = self.setHost,
active = self.state.guiEnabled, port = self.port,
onPortChange = self.setPort,
initDockState = Enum.InitialDockState.Right, onConnect = function()
initEnabled = false, self:startSession()
overridePreviousState = false, end,
floatingSize = Vector2.new(300, 200),
minimumSize = Vector2.new(300, 200),
zIndexBehavior = Enum.ZIndexBehavior.Sibling, onNavigateSettings = function()
onInitialState = function(initialState)
self:setState({ self:setState({
guiEnabled = initialState, appStatus = AppStatus.Settings,
}) })
end, end,
}),
Connecting = createPageElement(AppStatus.Connecting),
Connected = createPageElement(AppStatus.Connected, {
projectName = self.state.projectName,
address = self.state.address,
onDisconnect = function()
self:endSession()
end,
}),
Settings = createPageElement(AppStatus.Settings, {
onBack = function()
self:setState({
appStatus = AppStatus.NotConnected,
})
end,
}),
Error = createPageElement(AppStatus.Error, {
errorMessage = self.state.errorMessage,
onClose = function() onClose = function()
self:setState({ self:setState({
guiEnabled = false, appStatus = AppStatus.NotConnected,
toolbarIcon = Assets.Images.PluginButton,
}) })
end, end,
}, {
NotConnectedPage = PluginSettings.with(function(settings)
return createPageElement(AppStatus.NotConnected, {
onConnect = function(host, port)
self:startSession(host, port, {
openScriptsExternally = settings:get("openScriptsExternally"),
twoWaySync = settings:get("twoWaySync"),
})
end,
onNavigateSettings = function()
self:setState({
appStatus = AppStatus.Settings,
})
end,
})
end),
Connecting = createPageElement(AppStatus.Connecting),
Connected = createPageElement(AppStatus.Connected, {
projectName = self.state.projectName,
address = self.state.address,
onDisconnect = function()
Log.trace("Disconnecting session")
self.serveSession:stop()
self.serveSession = nil
self:setState({
appStatus = AppStatus.NotConnected,
})
Log.trace("Session terminated by user")
end,
}),
Settings = createPageElement(AppStatus.Settings, {
onBack = function()
self:setState({
appStatus = AppStatus.NotConnected,
})
end,
}),
Error = createPageElement(AppStatus.Error, {
errorMessage = self.state.errorMessage,
onClose = function()
self:setState({
appStatus = AppStatus.NotConnected,
})
end,
}),
Background = Theme.with(function(theme)
return e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = theme.BackgroundColor,
ZIndex = 0,
BorderSizePixel = 0,
})
end),
}), }),
toolbar = e(StudioToolbar, { Background = Theme.with(function(theme)
name = pluginName, return e("Frame", {
}, { Size = UDim2.new(1, 0, 1, 0),
button = e(StudioToggleButton, { BackgroundColor3 = theme.BackgroundColor,
name = "Rojo", ZIndex = 0,
tooltip = "Show or hide the Rojo panel", BorderSizePixel = 0,
icon = Assets.Images.PluginButton,
active = self.state.guiEnabled,
enabled = true,
onClick = function()
self:setState(function(state)
return {
guiEnabled = not state.guiEnabled,
}
end)
end,
}) })
}), end),
}),
toggleAction = e(StudioPluginAction, {
name = "RojoConnection",
title = "Rojo: Connect/Disconnect",
description = "Toggles the server for a Rojo sync session",
icon = Assets.Images.PluginButton,
bindable = true,
onTriggered = function()
if self.serveSession == nil or self.serveSession:getStatus() == ServeSession.Status.NotStarted then
self:startSession()
elseif self.serveSession ~= nil and self.serveSession:getStatus() == ServeSession.Status.Connected then
self:endSession()
end
end,
}),
connectAction = e(StudioPluginAction, {
name = "RojoConnect",
title = "Rojo: Connect",
description = "Connects the server for a Rojo sync session",
icon = Assets.Images.PluginButton,
bindable = true,
onTriggered = function()
if self.serveSession == nil or self.serveSession:getStatus() == ServeSession.Status.NotStarted then
self:startSession()
end
end,
}),
disconnectAction = e(StudioPluginAction, {
name = "RojoDisconnect",
title = "Rojo: Disconnect",
description = "Disconnects the server for a Rojo sync session",
icon = Assets.Images.PluginButton,
bindable = true,
onTriggered = function()
if self.serveSession ~= nil and self.serveSession:getStatus() == ServeSession.Status.Connected then
self:endSession()
end
end,
}),
toolbar = e(StudioToolbar, {
name = pluginName,
}, {
button = e(StudioToggleButton, {
name = "Rojo",
tooltip = "Show or hide the Rojo panel",
icon = self.state.toolbarIcon,
active = self.state.guiEnabled,
enabled = true,
onClick = function()
self:setState(function(state)
return {
guiEnabled = not state.guiEnabled,
}
end)
end,
})
}), }),
}), }),
}) })
end end
return App return function(props)
return e(PluginSettings.StudioProvider, {
plugin = props.plugin,
}, {
App = PluginSettings.with(function(settings)
local settingsProps = Dictionary.merge(props, {
settings = settings,
})
return e(App, settingsProps)
end),
})
end

View File

@@ -18,6 +18,8 @@ local Assets = {
Images = { Images = {
Logo = "rbxassetid://5990772764", Logo = "rbxassetid://5990772764",
PluginButton = "rbxassetid://3405341609", PluginButton = "rbxassetid://3405341609",
PluginButtonConnected = "rbxassetid://9529783993",
PluginButtonWarning = "rbxassetid://9529784530",
Icons = { Icons = {
Close = "rbxassetid://6012985953", Close = "rbxassetid://6012985953",
Back = "rbxassetid://6017213752", Back = "rbxassetid://6017213752",

View File

@@ -0,0 +1,40 @@
--[[
Take an InstanceMap and a dictionary mapping instances to sets of property
names. Populate a patch with the encoded values of all the given properties
on all the given instances (or, if any changes set Parent to nil, removals
of instances) and return the patch.
]]
local Log = require(script.Parent.Parent.Parent.Log)
local PatchSet = require(script.Parent.Parent.PatchSet)
local encodePatchUpdate = require(script.Parent.encodePatchUpdate)
return function(instanceMap, propertyChanges)
local patch = PatchSet.newEmpty()
for instance, properties in pairs(propertyChanges) do
local instanceId = instanceMap.fromInstances[instance]
if instanceId == nil then
Log.warn("Ignoring change for instance {:?} as it is unknown to Rojo", instance)
continue
end
if properties.Parent then
if instance.Parent == nil then
table.insert(patch.removed, instanceId)
else
Log.warn("Cannot sync non-nil Parent property changes yet")
end
else
local update = encodePatchUpdate(instance, instanceId, properties)
table.insert(patch.updated, update)
end
propertyChanges[instance] = nil
end
return patch
end

View File

@@ -0,0 +1,74 @@
return function()
local PatchSet = require(script.Parent.Parent.PatchSet)
local InstanceMap = require(script.Parent.Parent.InstanceMap)
local createPatchSet = require(script.Parent.createPatchSet)
it("should return a patch", function()
local patch = createPatchSet(InstanceMap.new(), {})
assert(PatchSet.validate(patch))
end)
it("should contain updates for every instance with property changes", function()
local instanceMap = InstanceMap.new()
local part1 = Instance.new("Part")
instanceMap:insert("PART_1", part1)
local part2 = Instance.new("Part")
instanceMap:insert("PART_2", part2)
local changes = {
[part1] = {
Position = true,
Size = true,
Color = true,
},
[part2] = {
CFrame = true,
Velocity = true,
Transparency = true,
},
}
local patch = createPatchSet(instanceMap, changes)
expect(#patch.updated).to.equal(2)
end)
it("should not contain any updates for removed instances", function()
local instanceMap = InstanceMap.new()
local part1 = Instance.new("Part")
instanceMap:insert("PART_1", part1)
local changes = {
[part1] = {
Parent = true,
Position = true,
Size = true,
},
}
local patch = createPatchSet(instanceMap, changes)
expect(#patch.removed).to.equal(1)
expect(#patch.updated).to.equal(0)
end)
it("should remove instances from the property change table", function()
local instanceMap = InstanceMap.new()
local part1 = Instance.new("Part")
instanceMap:insert("PART_1", part1)
local changes = {
[part1] = {},
}
createPatchSet(instanceMap, changes)
expect(next(changes)).to.equal(nil)
end)
end

View File

@@ -0,0 +1,39 @@
local Log = require(script.Parent.Parent.Parent.Log)
local RbxDom = require(script.Parent.Parent.Parent.RbxDom)
local encodeProperty = require(script.Parent.encodeProperty)
return function(instance, instanceId, properties)
local update = {
id = instanceId,
changedProperties = {},
}
for propertyName in pairs(properties) do
if propertyName == "Name" then
update.changedName = instance.Name
else
local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
if not descriptor then
Log.debug("Could not sync back property {:?}.{}", instance, propertyName)
continue
end
local encodeSuccess, encodeResult = encodeProperty(instance, propertyName, descriptor)
if not encodeSuccess then
Log.debug("Could not sync back property {:?}.{}: {}", instance, propertyName, encodeResult)
continue
end
update.changedProperties[propertyName] = encodeResult
end
end
if next(update.changedProperties) == nil and update.changedName == nil then
return nil
end
return update
end

View File

@@ -0,0 +1,62 @@
return function()
local encodePatchUpdate = require(script.Parent.encodePatchUpdate)
it("should return an update when there are property changes", function()
local part = Instance.new("Part")
local properties = {
CFrame = true,
Color = true,
}
local update = encodePatchUpdate(part, "PART", properties)
expect(update.id).to.equal("PART")
expect(update.changedProperties.CFrame).to.be.ok()
expect(update.changedProperties.Color).to.be.ok()
end)
it("should return nil when there are no property changes", function()
local part = Instance.new("Part")
local properties = {
NonExistentProperty = true,
}
local update = encodePatchUpdate(part, "PART", properties)
expect(update).to.equal(nil)
end)
it("should set changedName in the update when the instance's Name changes", function()
local part = Instance.new("Part")
local properties = {
Name = true,
}
part.Name = "We'reGettingToTheCoolPart"
local update = encodePatchUpdate(part, "PART", properties)
expect(update.changedName).to.equal("We'reGettingToTheCoolPart")
end)
it("should correctly encode property values", function()
local part = Instance.new("Part")
local properties = {
Position = true,
Color = true,
}
part.Position = Vector3.new(0, 100, 0)
part.Color = Color3.new(0.8, 0.2, 0.9)
local update = encodePatchUpdate(part, "PART", properties)
local position = update.changedProperties.Position
local color = update.changedProperties.Color
expect(position.Vector3[1]).to.equal(0)
expect(position.Vector3[2]).to.equal(100)
expect(position.Vector3[3]).to.equal(0)
expect(color.Color3[1]).to.be.near(0.8, 0.01)
expect(color.Color3[2]).to.be.near(0.2, 0.01)
expect(color.Color3[3]).to.be.near(0.9, 0.01)
end)
end

View File

@@ -0,0 +1,21 @@
local Log = require(script.Parent.Parent.Parent.Log)
local RbxDom = require(script.Parent.Parent.Parent.RbxDom)
return function(instance, propertyName, propertyDescriptor)
local readSuccess, readResult = propertyDescriptor:read(instance)
if not readSuccess then
Log.warn("Could not sync back property {:?}.{}: {}", instance, propertyName, readResult)
return false, nil
end
local dataType = propertyDescriptor.dataType
local encodeSuccess, encodeResult = RbxDom.EncodedValue.encode(readResult, dataType)
if not encodeSuccess then
Log.warn("Could not sync back property {:?}.{}: {}", instance, propertyName, encodeResult)
return false, nil
end
return true, encodeResult
end

View File

@@ -0,0 +1,81 @@
--[[
The ChangeBatcher is responsible for collecting and dispatching changes made
to tracked instances during two-way sync.
]]
local RunService = game:GetService("RunService")
local PatchSet = require(script.Parent.PatchSet)
local createPatchSet = require(script.createPatchSet)
local ChangeBatcher = {}
ChangeBatcher.__index = ChangeBatcher
local BATCH_INTERVAL = 0.2
function ChangeBatcher.new(instanceMap, onChangesFlushed)
local self
local renderSteppedConnection = RunService.RenderStepped:Connect(function(dt)
self:__cycle(dt)
end)
self = setmetatable({
__accumulator = 0,
__renderSteppedConnection = renderSteppedConnection,
__instanceMap = instanceMap,
__onChangesFlushed = onChangesFlushed,
__pendingPropertyChanges = {},
}, ChangeBatcher)
return self
end
function ChangeBatcher:stop()
self.__renderSteppedConnection:Disconnect()
self.__pendingPropertyChanges = {}
end
function ChangeBatcher:add(instance, propertyName)
local properties = self.__pendingPropertyChanges[instance]
if not properties then
properties = {}
self.__pendingPropertyChanges[instance] = properties
end
properties[propertyName] = true
end
function ChangeBatcher:__cycle(dt)
self.__accumulator += dt
if self.__accumulator >= BATCH_INTERVAL then
self.__accumulator -= BATCH_INTERVAL
local patch = self:__flush()
if patch then
self.__onChangesFlushed(patch)
end
end
self.__instanceMap:unpauseAllInstances()
end
function ChangeBatcher:__flush()
if next(self.__pendingPropertyChanges) == nil then
return nil
end
local patch = createPatchSet(self.__instanceMap, self.__pendingPropertyChanges)
if PatchSet.isEmpty(patch) then
return nil
end
return patch
end
return ChangeBatcher

View File

@@ -0,0 +1,101 @@
return function()
local ChangeBatcher = require(script.Parent)
local InstanceMap = require(script.Parent.Parent.InstanceMap)
local PatchSet = require(script.Parent.Parent.PatchSet)
local noop = function() end
describe("new", function()
it("should create a new ChangeBatcher", function()
local instanceMap = InstanceMap.new()
local changeBatcher = ChangeBatcher.new(instanceMap, noop)
expect(changeBatcher.__pendingPropertyChanges).to.be.a("table")
expect(next(changeBatcher.__pendingPropertyChanges)).to.equal(nil)
expect(changeBatcher.__onChangesFlushed).to.equal(noop)
expect(changeBatcher.__instanceMap).to.equal(instanceMap)
expect(typeof(changeBatcher.__renderSteppedConnection)).to.equal("RBXScriptConnection")
end)
end)
describe("stop", function()
it("should disconnect the RenderStepped connection", function()
local changeBatcher = ChangeBatcher.new(InstanceMap.new(), noop)
changeBatcher:stop()
expect(changeBatcher.__renderSteppedConnection.Connected).to.equal(false)
end)
end)
describe("add", function()
it("should add property changes to be considered for the current batch", function()
local instanceMap = InstanceMap.new()
local changeBatcher = ChangeBatcher.new(instanceMap, noop)
local part = Instance.new("Part")
instanceMap:insert("PART", part)
changeBatcher:add(part, "Name")
local properties = changeBatcher.__pendingPropertyChanges[part]
expect(properties).to.be.a("table")
expect(properties.Name).to.be.ok()
changeBatcher:add(part, "Position")
expect(properties.Position).to.be.ok()
end)
end)
describe("__cycle", function()
it("should immediately unpause any paused instances after each cycle", function()
local instanceMap = InstanceMap.new()
local changeBatcher = ChangeBatcher.new(instanceMap, noop)
local part = Instance.new("Part")
instanceMap.pausedUpdateInstances[part] = true
changeBatcher:__cycle(0)
expect(instanceMap.pausedUpdateInstances[part]).to.equal(nil)
end)
end)
describe("__flush", function()
it("should return nil when there are no changes to process", function()
local changeBatcher = ChangeBatcher.new(InstanceMap.new(), noop)
expect(changeBatcher:__flush()).to.equal(nil)
end)
it("should return a patch when there are changes to process and the resulting patch is non-empty", function()
local instanceMap = InstanceMap.new()
local changeBatcher = ChangeBatcher.new(instanceMap, noop)
local part = Instance.new("Part")
instanceMap:insert("PART", part)
changeBatcher.__pendingPropertyChanges[part] = {
Position = true,
Name = true,
}
local patch = changeBatcher:__flush()
assert(PatchSet.validate(patch))
end)
it("should return nil when there are changes to process and the resulting patch is empty", function()
local instanceMap = InstanceMap.new()
local changeBatcher = ChangeBatcher.new(instanceMap, noop)
local part = Instance.new("Part")
instanceMap:insert("PART", part)
changeBatcher.__pendingPropertyChanges[part] = {
NonExistentProperty = true,
}
expect(changeBatcher:__flush()).to.equal(nil)
end)
end)
end

View File

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

View File

@@ -1,3 +1,5 @@
local RunService = game:GetService("RunService")
local Log = require(script.Parent.Parent.Log) local Log = require(script.Parent.Parent.Log)
--[[ --[[
@@ -135,29 +137,31 @@ function InstanceMap:destroyId(id)
end end
--[[ --[[
Pause updates for an instance momentarily and invoke a callback. Pause updates for an instance.
If the callback throws an error, InstanceMap will still be kept in a
consistent state.
]] ]]
function InstanceMap:pauseInstance(instance, callback) function InstanceMap:pauseInstance(instance)
local id = self.fromInstances[instance] local id = self.fromInstances[instance]
-- If we don't know about this instance, ignore it and do not invoke the -- If we don't know about this instance, ignore it.
-- callback.
if id == nil then if id == nil then
return return
end end
self.pausedUpdateInstances[instance] = true self.pausedUpdateInstances[instance] = true
local success, result = xpcall(callback, debug.traceback) end
self.pausedUpdateInstances[instance] = false
if success then --[[
return result Unpause updates for an instance.
else ]]
error(result, 2) function InstanceMap:unpauseInstance(instance)
end self.pausedUpdateInstances[instance] = nil
end
--[[
Unpause updates for all instances.
]]
function InstanceMap:unpauseAllInstances()
table.clear(self.pausedUpdateInstances)
end end
function InstanceMap:__connectSignals(instance) function InstanceMap:__connectSignals(instance)
@@ -200,6 +204,12 @@ function InstanceMap:__maybeFireInstanceChanged(instance, propertyName)
return return
end end
if RunService:IsRunning() then
-- We probably don't want to pick up property changes to save to the
-- filesystem in a running game.
return
end
self.onInstanceChanged(instance, propertyName) self.onInstanceChanged(instance, propertyName)
end end
@@ -222,4 +232,4 @@ function InstanceMap:__disconnectSignals(instance)
end end
end end
return InstanceMap return InstanceMap

View File

@@ -63,7 +63,7 @@ local function applyPatch(instanceMap, patch)
local failedToReify = reify(instanceMap, patch.added, id, parentInstance) local failedToReify = reify(instanceMap, patch.added, id, parentInstance)
if not PatchSet.isEmpty(failedToReify) then if not PatchSet.isEmpty(failedToReify) then
Log.debug("Failed to reify as part of applying a patch: {}", failedToReify) Log.debug("Failed to reify as part of applying a patch: {:#?}", failedToReify)
PatchSet.assign(unappliedPatch, failedToReify) PatchSet.assign(unappliedPatch, failedToReify)
end end
end end
@@ -77,6 +77,10 @@ local function applyPatch(instanceMap, patch)
continue continue
end end
-- Pause updates on this instance to avoid picking up our changes when
-- two-way sync is enabled.
instanceMap:pauseInstance(instance)
-- Track any part of this update that could not be applied. -- Track any part of this update that could not be applied.
local unappliedUpdate = { local unappliedUpdate = {
id = update.id, id = update.id,
@@ -197,4 +201,4 @@ local function applyPatch(instanceMap, patch)
return unappliedPatch return unappliedPatch
end end
return applyPatch return applyPatch

View File

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

View File

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

View File

@@ -75,7 +75,13 @@ local function diff(instanceMap, virtualInstances, rootId)
changedProperties[propertyName] = virtualValue changedProperties[propertyName] = virtualValue
end end
else else
Log.warn("Failed to decode property of type {}", virtualValue.Type) local propertyType = next(virtualValue)
Log.warn(
"Failed to decode property {}.{}. Encoded property was: {:#?}",
virtualInstance.ClassName,
propertyName,
virtualValue
)
end end
else else
local err = existingValueOrErr local err = existingValueOrErr

View File

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

View File

@@ -40,6 +40,13 @@ local function getProperty(instance, propertyName)
}) })
end end
if err.kind == RbxDom.Error.Kind.Roblox and err.extra:find("is not a valid member of") then
return false, Error.new(Error.UnknownProperty, {
className = instance.ClassName,
propertyName = propertyName,
})
end
return false, Error.new(Error.OtherPropertyError, { return false, Error.new(Error.OtherPropertyError, {
className = instance.ClassName, className = instance.ClassName,
propertyName = propertyName, propertyName = propertyName,

View File

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

View File

@@ -54,8 +54,7 @@ return function()
Name = "Spaghetti", Name = "Spaghetti",
Properties = { Properties = {
Value = { Value = {
Type = "String", String = "Hello, world!",
Value = "Hello, world!",
}, },
}, },
Children = {}, Children = {},
@@ -191,8 +190,7 @@ return function()
Name = "Child", Name = "Child",
Properties = { Properties = {
Value = { Value = {
Type = "Ref", Ref = "ROOT",
Value = "ROOT",
}, },
}, },
Children = {}, Children = {},
@@ -219,8 +217,7 @@ return function()
Name = "Root", Name = "Root",
Properties = { Properties = {
Value = { Value = {
Type = "Ref", Ref = "EXISTING",
Value = "EXISTING",
}, },
}, },
Children = {}, Children = {},
@@ -258,8 +255,7 @@ return function()
Name = "Child A", Name = "Child A",
Properties = { Properties = {
Value = { Value = {
Type = "Ref", Ref = "CHILD_B",
Value = "Child B",
}, },
}, },
Children = {}, Children = {},
@@ -291,15 +287,14 @@ return function()
-- constructed as part of a recursive call before the parent has totally -- constructed as part of a recursive call before the parent has totally
-- finished. Given deferred refs, this should not fail, but it is a good -- finished. Given deferred refs, this should not fail, but it is a good
-- case to test. -- case to test.
it("should apply properties containing refs to later siblings correctly", function() it("should apply properties containing refs to later children correctly", function()
local virtualInstances = { local virtualInstances = {
ROOT = { ROOT = {
ClassName = "ObjectValue", ClassName = "ObjectValue",
Name = "Root", Name = "Root",
Properties = { Properties = {
Value = { Value = {
Type = "Ref", Ref = "CHILD",
Value = "CHILD",
}, },
}, },
Children = {"CHILD"}, Children = {"CHILD"},
@@ -349,4 +344,4 @@ return function()
expect(update.id).to.equal("ROOT") expect(update.id).to.equal("ROOT")
expect(update.changedProperties.Value).to.equal(virtualInstances["ROOT"].Properties.Value) expect(update.changedProperties.Value).to.equal(virtualInstances["ROOT"].Properties.Value)
end) end)
end end

View File

@@ -5,6 +5,7 @@ local Log = require(script.Parent.Parent.Log)
local Fmt = require(script.Parent.Parent.Fmt) local Fmt = require(script.Parent.Parent.Fmt)
local t = require(script.Parent.Parent.t) local t = require(script.Parent.Parent.t)
local ChangeBatcher = require(script.Parent.ChangeBatcher)
local InstanceMap = require(script.Parent.InstanceMap) local InstanceMap = require(script.Parent.InstanceMap)
local PatchSet = require(script.Parent.PatchSet) local PatchSet = require(script.Parent.PatchSet)
local Reconciler = require(script.Parent.Reconciler) local Reconciler = require(script.Parent.Reconciler)
@@ -56,10 +57,19 @@ function ServeSession.new(options)
-- Declare self ahead of time to capture it in a closure -- Declare self ahead of time to capture it in a closure
local self local self
local function onInstanceChanged(instance, propertyName) local function onInstanceChanged(instance, propertyName)
self:__onInstanceChanged(instance, propertyName) if not self.__twoWaySync then
return
end
self.__changeBatcher:add(instance, propertyName)
end
local function onChangesFlushed(patch)
self.__apiContext:write(patch)
end end
local instanceMap = InstanceMap.new(onInstanceChanged) local instanceMap = InstanceMap.new(onInstanceChanged)
local changeBatcher = ChangeBatcher.new(instanceMap, onChangesFlushed)
local reconciler = Reconciler.new(instanceMap) local reconciler = Reconciler.new(instanceMap)
local connections = {} local connections = {}
@@ -82,6 +92,7 @@ function ServeSession.new(options)
__twoWaySync = options.twoWaySync, __twoWaySync = options.twoWaySync,
__reconciler = reconciler, __reconciler = reconciler,
__instanceMap = instanceMap, __instanceMap = instanceMap,
__changeBatcher = changeBatcher,
__statusChangedCallback = nil, __statusChangedCallback = nil,
__connections = connections, __connections = connections,
} }
@@ -102,6 +113,10 @@ function ServeSession:__fmtDebug(output)
output:write("}") output:write("}")
end end
function ServeSession:getStatus()
return self.__status
end
function ServeSession:onStatusChanged(callback) function ServeSession:onStatusChanged(callback)
self.__statusChangedCallback = callback self.__statusChangedCallback = callback
end end
@@ -179,55 +194,6 @@ function ServeSession:__onActiveScriptChanged(activeScript)
self.__apiContext:open(scriptId) self.__apiContext:open(scriptId)
end end
function ServeSession:__onInstanceChanged(instance, propertyName)
if not self.__twoWaySync then
return
end
local instanceId = self.__instanceMap.fromInstances[instance]
if instanceId == nil then
Log.warn("Ignoring change for instance {:?} as it is unknown to Rojo", instance)
return
end
local remove = nil
local update = {
id = instanceId,
changedProperties = {},
}
if propertyName == "Name" then
update.changedName = instance.Name
elseif propertyName == "Parent" then
if instance.Parent == nil then
update = nil
remove = instanceId
else
Log.warn("Cannot sync non-nil Parent property changes yet")
return
end
else
local success, encoded = self.__reconciler:encodeApiValue(instance[propertyName])
if not success then
Log.warn("Could not sync back property {:?}.{}", instance, propertyName)
return
end
update.changedProperties[propertyName] = encoded
end
local patch = {
removed = {remove},
added = {},
updated = {update},
}
self.__apiContext:write(patch)
end
function ServeSession:__initialSync(rootInstanceId) function ServeSession:__initialSync(rootInstanceId)
return self.__apiContext:read({ rootInstanceId }) return self.__apiContext:read({ rootInstanceId })
:andThen(function(readResponseBody) :andThen(function(readResponseBody)
@@ -290,6 +256,7 @@ function ServeSession:__stopInternal(err)
self:__setStatus(Status.Disconnected, err) self:__setStatus(Status.Disconnected, err)
self.__apiContext:disconnect() self.__apiContext:disconnect()
self.__instanceMap:stop() self.__instanceMap:stop()
self.__changeBatcher:stop()
for _, connection in ipairs(self.__connections) do for _, connection in ipairs(self.__connections) do
connection:Disconnect() connection:Disconnect()
@@ -305,4 +272,4 @@ function ServeSession:__setStatus(status, detail)
end end
end end
return ServeSession return ServeSession

View File

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

View File

@@ -9,7 +9,7 @@
}, },
"TestEZ": { "TestEZ": {
"$path": "modules/testez/lib" "$path": "modules/testez"
} }
}, },
@@ -25,4 +25,4 @@
} }
} }
} }
} }

View File

@@ -0,0 +1,24 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">optional</string>
</Properties>
<Item class="StringValue" referent="1">
<Properties>
<string name="Name">foo-optional</string>
<string name="Value">Hello, from foo.txt!</string>
</Properties>
</Item>
<Item class="StringValue" referent="2">
<Properties>
<string name="Name">foo-required</string>
<string name="Value">Hello, from foo.txt!</string>
</Properties>
</Item>
</Item>
</roblox>

View File

@@ -0,0 +1,17 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">root</string>
</Properties>
<Item class="Folder" referent="1">
<Properties>
<string name="Name">folder</string>
</Properties>
</Item>
</Item>
</roblox>

View File

@@ -0,0 +1,22 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">root</string>
</Properties>
<Item class="Folder" referent="1">
<Properties>
<string name="Name">folder</string>
</Properties>
<Item class="Folder" referent="2">
<Properties>
<string name="Name">child-projectname</string>
</Properties>
</Item>
</Item>
</Item>
</roblox>

View File

@@ -0,0 +1,12 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">root</string>
</Properties>
</Item>
</roblox>

View File

@@ -1,6 +1,7 @@
--- ---
source: tests/tests/build.rs source: tests/tests/build.rs
expression: contents expression: contents
--- ---
<roblox version="4"> <roblox version="4">
<Item class="Folder" referent="0"> <Item class="Folder" referent="0">
@@ -25,14 +26,12 @@ expression: contents
<R22>1</R22> <R22>1</R22>
</CoordinateFrame> </CoordinateFrame>
<Ref name="PrimaryPart">null</Ref> <Ref name="PrimaryPart">null</Ref>
<BinaryString name="Tags"> <BinaryString name="Tags"></BinaryString>
</BinaryString>
</Properties> </Properties>
<Item class="StringValue" referent="2"> <Item class="StringValue" referent="2">
<Properties> <Properties>
<string name="Name">Cool StringValue</string> <string name="Name">Cool StringValue</string>
<BinaryString name="Tags"> <BinaryString name="Tags"></BinaryString>
</BinaryString>
<string name="Value">Did you know that BaseValue.Changed is different than Instance.Changed?</string> <string name="Value">Did you know that BaseValue.Changed is different than Instance.Changed?</string>
</Properties> </Properties>
</Item> </Item>

View File

@@ -1,27 +1,25 @@
--- ---
source: tests/tests/build.rs source: tests/tests/build.rs
expression: contents expression: contents
--- ---
<roblox version="4"> <roblox version="4">
<Item class="Folder" referent="0"> <Item class="Folder" referent="0">
<Properties> <Properties>
<string name="Name">rbxmx_ref</string> <string name="Name">rbxmx_ref</string>
<BinaryString name="Tags"> <BinaryString name="Tags"></BinaryString>
</BinaryString>
</Properties> </Properties>
<Item class="StringValue" referent="1"> <Item class="StringValue" referent="1">
<Properties> <Properties>
<string name="Name">Target</string> <string name="Name">Target</string>
<BinaryString name="Tags"> <BinaryString name="Tags"></BinaryString>
</BinaryString>
<string name="Value">Pointed to by ObjectValue</string> <string name="Value">Pointed to by ObjectValue</string>
</Properties> </Properties>
</Item> </Item>
<Item class="ObjectValue" referent="2"> <Item class="ObjectValue" referent="2">
<Properties> <Properties>
<string name="Name">Pointer</string> <string name="Name">Pointer</string>
<BinaryString name="Tags"> <BinaryString name="Tags"></BinaryString>
</BinaryString>
<Ref name="Value">1</Ref> <Ref name="Value">1</Ref>
</Properties> </Properties>
</Item> </Item>

View File

@@ -1,6 +1,7 @@
--- ---
source: tests/tests/build.rs source: tests/tests/build.rs
expression: contents expression: contents
--- ---
<roblox version="4"> <roblox version="4">
<Item class="DataModel" referent="0"> <Item class="DataModel" referent="0">
@@ -31,11 +32,21 @@ expression: contents
<Item class="Part" referent="4"> <Item class="Part" referent="4">
<Properties> <Properties>
<string name="Name">Color</string> <string name="Name">Color</string>
<Color3 name="Color3uint8"> <CoordinateFrame name="CFrame">
<R>0.5</R> <X>1</X>
<G>0.25</G> <Y>2</Y>
<B>0</B> <Z>3</Z>
</Color3> <R00>0</R00>
<R01>1</R01>
<R02>0</R02>
<R10>0</R10>
<R11>0</R11>
<R12>1</R12>
<R20>1</R20>
<R21>0</R21>
<R22>0</R22>
</CoordinateFrame>
<Color3uint8 name="Color3uint8">8404992</Color3uint8>
</Properties> </Properties>
</Item> </Item>
<Item class="NumberValue" referent="5"> <Item class="NumberValue" referent="5">

View File

@@ -0,0 +1,230 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">weldconstraint</string>
<BinaryString name="AttributesSerialize">
</BinaryString>
<int64 name="SourceAssetId">-1</int64>
<BinaryString name="Tags"></BinaryString>
</Properties>
<Item class="Part" referent="1">
<Properties>
<string name="Name">A</string>
<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">10724005</Color3uint8>
<PhysicalProperties name="CustomPhysicalProperties">
<CustomPhysics>false</CustomPhysics>
</PhysicalProperties>
<token name="formFactorRaw">1</token>
<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>
<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>
<token name="shape">1</token>
<Vector3 name="size">
<X>4</X>
<Y>1</Y>
<Z>2</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>
</Properties>
<Item class="WeldConstraint" referent="2">
<Properties>
<string name="Name">WeldConstraint</string>
<BinaryString name="AttributesSerialize">
</BinaryString>
<CoordinateFrame name="CFrame0">
<X>7</X>
<Y>0.000001013279</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>
<Ref name="Part0Internal">1</Ref>
<Ref name="Part1Internal">3</Ref>
<int64 name="SourceAssetId">-1</int64>
<int name="State">3</int>
<BinaryString name="Tags"></BinaryString>
</Properties>
</Item>
</Item>
<Item class="Part" referent="3">
<Properties>
<string name="Name">B</string>
<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.500001</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">10724005</Color3uint8>
<PhysicalProperties name="CustomPhysicalProperties">
<CustomPhysics>false</CustomPhysics>
</PhysicalProperties>
<token name="formFactorRaw">1</token>
<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>
<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>
<token name="shape">1</token>
<Vector3 name="size">
<X>4</X>
<Y>1</Y>
<Z>2</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>
</Properties>
</Item>
</Item>
</roblox>

View File

@@ -0,0 +1,15 @@
{
"name": "optional",
"tree": {
"$className": "Folder",
"foo-required": {
"$path": "foo.txt"
},
"foo-optional":{
"$path": { "optional": "foo.txt" }
},
"bar-optional":{
"$path": { "optional": "bar.txt" }
}
}
}

View File

@@ -0,0 +1 @@
Hello, from foo.txt!

View File

@@ -0,0 +1,9 @@
{
"name": "root",
"tree": {
"$className": "Folder",
"folder": {
"$path": "folder"
}
}
}

View File

@@ -0,0 +1,6 @@
{
"name": "child-projectname",
"tree": {
"$className": "Folder"
}
}

View File

@@ -0,0 +1,9 @@
{
"name": "root",
"tree": {
"$className": "Folder",
"folder": {
"$path": "folder"
}
}
}

View File

@@ -0,0 +1,6 @@
{
"name": "child-projectname",
"tree": {
"$className": "Folder"
}
}

View File

@@ -0,0 +1,6 @@
{
"name": "root",
"tree": {
"$className": "Folder"
}
}

View File

@@ -14,7 +14,13 @@
"Color": { "Color": {
"$className": "Part", "$className": "Part",
"$properties": { "$properties": {
"Color": [0.5, 0.25, 0] "Color": [0.5, 0.25, 0],
"CFrame": [
1, 2, 3,
0, 1, 0,
0, 0, 1,
1, 0, 0
]
} }
}, },

View File

@@ -0,0 +1,6 @@
{
"name": "weldconstraint",
"tree": {
"$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

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

View File

@@ -0,0 +1,62 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
Children:
- id-3
ClassName: Folder
Id: id-2
Metadata:
ignoreUnknownInstances: true
Name: optional
Parent: "00000000000000000000000000000000"
Properties: {}
id-3:
Children:
- id-4
- id-5
ClassName: Folder
Id: id-3
Metadata:
ignoreUnknownInstances: false
Name: src
Parent: id-2
Properties: {}
id-4:
Children: []
ClassName: StringValue
Id: id-4
Metadata:
ignoreUnknownInstances: false
Name: foo
Parent: id-3
Properties:
Value:
String: "Hello, from foo.txt!"
id-5:
Children:
- id-6
ClassName: Folder
Id: id-5
Metadata:
ignoreUnknownInstances: false
Name: node_modules
Parent: id-3
Properties: {}
id-6:
Children: []
ClassName: StringValue
Id: id-6
Metadata:
ignoreUnknownInstances: false
Name: bar
Parent: id-5
Properties:
Value:
String: Hello from bar.txt
messageCursor: 2
sessionId: id-1

View File

@@ -0,0 +1,40 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
Children:
- id-3
ClassName: Folder
Id: id-2
Metadata:
ignoreUnknownInstances: true
Name: optional
Parent: "00000000000000000000000000000000"
Properties: {}
id-3:
Children:
- id-4
ClassName: Folder
Id: id-3
Metadata:
ignoreUnknownInstances: false
Name: src
Parent: id-2
Properties: {}
id-4:
Children: []
ClassName: StringValue
Id: id-4
Metadata:
ignoreUnknownInstances: false
Name: foo
Parent: id-3
Properties:
Value:
String: "Hello, from foo.txt!"
messageCursor: 0
sessionId: id-1

View File

@@ -0,0 +1,36 @@
---
source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
---
messageCursor: 2
messages:
- added:
id-5:
Children:
- id-6
ClassName: Folder
Id: id-5
Metadata:
ignoreUnknownInstances: false
Name: node_modules
Parent: id-3
Properties: {}
id-6:
Children: []
ClassName: StringValue
Id: id-6
Metadata:
ignoreUnknownInstances: false
Name: bar
Parent: id-5
Properties:
Value:
String: Hello from bar.txt
removed: []
updated: []
- added: {}
removed: []
updated: []
sessionId: id-1

View File

@@ -1,6 +1,7 @@
--- ---
source: tests/tests/serve.rs source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)" expression: "read_response.intern_and_redact(&mut redactions, root_id)"
--- ---
instances: instances:
id-2: id-2:
@@ -13,7 +14,7 @@ instances:
Parent: "00000000000000000000000000000000" Parent: "00000000000000000000000000000000"
Properties: Properties:
Source: Source:
Type: String String: "-- Edited contents"
Value: "-- Edited contents"
messageCursor: 1 messageCursor: 1
sessionId: id-1 sessionId: id-1

View File

@@ -1,6 +1,7 @@
--- ---
source: tests/tests/serve.rs source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)" expression: "read_response.intern_and_redact(&mut redactions, root_id)"
--- ---
instances: instances:
id-2: id-2:
@@ -13,7 +14,7 @@ instances:
Parent: "00000000000000000000000000000000" Parent: "00000000000000000000000000000000"
Properties: Properties:
Source: Source:
Type: String String: "-- Original contents"
Value: "-- Original contents"
messageCursor: 0 messageCursor: 0
sessionId: id-1 sessionId: id-1

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
--- ---
source: tests/tests/serve.rs source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)" expression: "read_response.intern_and_redact(&mut redactions, root_id)"
--- ---
instances: instances:
id-2: id-2:
@@ -23,7 +24,7 @@ instances:
Parent: id-2 Parent: id-2
Properties: Properties:
Value: Value:
Type: String String: This file will be removed!
Value: This file will be removed!
messageCursor: 0 messageCursor: 0
sessionId: id-1 sessionId: id-1

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
{
"name": "optional",
"tree": {
"$className": "Folder",
"create-later": {
"$path": { "optional": "create-later" }
}
}
}

View File

@@ -1,4 +1,4 @@
std = "roblox" std = "roblox+testez"
[config] [config]
unused_variable = { allow_unused_self = true } unused_variable = { allow_unused_self = true }

View File

@@ -16,6 +16,9 @@ use crate::{
snapshot_middleware::{snapshot_from_vfs, snapshot_project_node}, snapshot_middleware::{snapshot_from_vfs, snapshot_project_node},
}; };
/// Processes file change events, updates the DOM, and sends those updates
/// through a channel for other stuff to consume.
///
/// Owns the connection between Rojo's VFS and its DOM by holding onto another /// Owns the connection between Rojo's VFS and its DOM by holding onto another
/// thread that processes messages. /// thread that processes messages.
/// ///
@@ -281,22 +284,14 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
// that path and use it as the source for our patch. // that path and use it as the source for our patch.
let snapshot = match snapshot_from_vfs(&metadata.context, &vfs, &path) { let snapshot = match snapshot_from_vfs(&metadata.context, &vfs, &path) {
Ok(Some(snapshot)) => snapshot, Ok(snapshot) => snapshot,
Ok(None) => {
log::error!(
"Snapshot did not return an instance from path {}",
path.display()
);
log::error!("This may be a bug!");
return None;
}
Err(err) => { Err(err) => {
log::error!("Snapshot error: {:?}", err); log::error!("Snapshot error: {:?}", err);
return None; return None;
} }
}; };
let patch_set = compute_patch_set(&snapshot, &tree, id); let patch_set = compute_patch_set(snapshot.as_ref(), &tree, id);
apply_patch_set(tree, patch_set) apply_patch_set(tree, patch_set)
} }
Ok(None) => { Ok(None) => {
@@ -332,19 +327,14 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
); );
let snapshot = match snapshot_result { let snapshot = match snapshot_result {
Ok(Some(snapshot)) => snapshot, Ok(snapshot) => snapshot,
Ok(None) => {
log::error!("Snapshot did not return an instance from a project node.");
log::error!("This is a bug!");
return None;
}
Err(err) => { Err(err) => {
log::error!("{:?}", err); log::error!("{:?}", err);
return None; return None;
} }
}; };
let patch_set = compute_patch_set(&snapshot, &tree, id); let patch_set = compute_patch_set(snapshot.as_ref(), &tree, id);
apply_patch_set(tree, patch_set) apply_patch_set(tree, patch_set)
} }
}; };

View File

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

View File

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

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

View File

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

View File

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

View File

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

136
src/cli/sourcemap.rs Normal file
View File

@@ -0,0 +1,136 @@
use std::{
io::{BufWriter, Write},
path::{Path, PathBuf},
};
use fs_err::File;
use memofs::Vfs;
use rbx_dom_weak::types::Ref;
use serde::Serialize;
use structopt::StructOpt;
use crate::{
serve_session::ServeSession,
snapshot::{InstanceWithMeta, RojoTree},
};
use super::resolve_path;
const PATH_STRIP_FAILED_ERR: &str = "Failed to create relative paths for project file!";
/// Representation of a node in the generated sourcemap tree.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct SourcemapNode {
name: String,
class_name: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
file_paths: Vec<PathBuf>,
#[serde(skip_serializing_if = "Vec::is_empty")]
children: Vec<SourcemapNode>,
}
/// Generates a sourcemap file from the Rojo project.
#[derive(Debug, StructOpt)]
pub struct SourcemapCommand {
/// Path to the project to use for the sourcemap. Defaults to the current
/// directory.
#[structopt(default_value = "")]
pub project: PathBuf,
/// Where to output the sourcemap. Omit this to use stdout instead of
/// writing to a file.
///
/// Should end in .json.
#[structopt(long, short)]
pub output: Option<PathBuf>,
/// If non-script files should be included or not. Defaults to false.
#[structopt(long)]
pub include_non_scripts: bool,
}
impl SourcemapCommand {
pub fn run(self) -> anyhow::Result<()> {
let project_path = resolve_path(&self.project);
log::trace!("Constructing in-memory filesystem");
let vfs = Vfs::new_default();
let session = ServeSession::new(vfs, &project_path)?;
let tree = session.tree();
let filter = if self.include_non_scripts {
filter_nothing
} else {
filter_non_scripts
};
let root_node = recurse_create_node(&tree, tree.get_root_id(), session.root_dir(), filter);
if let Some(output_path) = self.output {
let mut file = BufWriter::new(File::create(&output_path)?);
serde_json::to_writer(&mut file, &root_node)?;
file.flush()?;
println!("Created sourcemap at {}", output_path.display());
} else {
let output = serde_json::to_string(&root_node)?;
println!("{}", output);
}
Ok(())
}
}
fn filter_nothing(_instance: &InstanceWithMeta) -> bool {
true
}
fn filter_non_scripts(instance: &InstanceWithMeta) -> bool {
match instance.class_name() {
"Script" | "LocalScript" | "ModuleScript" => true,
_ => false,
}
}
fn recurse_create_node(
tree: &RojoTree,
referent: Ref,
project_dir: &Path,
filter: fn(&InstanceWithMeta) -> bool,
) -> Option<SourcemapNode> {
let instance = tree.get_instance(referent).expect("instance did not exist");
let mut children = Vec::new();
for &child_id in instance.children() {
if let Some(child_node) = recurse_create_node(tree, child_id, &project_dir, filter) {
children.push(child_node);
}
}
// If this object has no children and doesn't pass the filter, it doesn't
// contain any information we're looking for.
if children.is_empty() && !filter(&instance) {
return None;
}
let file_paths = instance
.metadata()
.relevant_paths
.iter()
// Not all paths listed as relevant are guaranteed to exist.
.filter(|path| path.is_file())
.map(|path| path.strip_prefix(project_dir).expect(PATH_STRIP_FAILED_ERR))
.map(|path| path.to_path_buf())
.collect();
Some(SourcemapNode {
name: instance.name().to_string(),
class_name: instance.class_name().to_string(),
file_paths,
children,
})
}

View File

@@ -1,46 +1,120 @@
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::{bail, format_err, Context};
use memofs::Vfs; use memofs::Vfs;
use reqwest::{ use reqwest::{
header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT}, header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT},
StatusCode, 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)] use super::resolve_path;
enum Error {
#[error("Rojo could not find your Roblox auth cookie. Please pass one via --cookie.")]
NeedAuthCookie,
#[error("The Roblox API returned an unexpected error: {body}")] /// Builds the project and uploads it to Roblox.
RobloxApi { body: String }, #[derive(Debug, StructOpt)]
pub struct UploadCommand {
/// Path to the project to upload. Defaults to the current directory.
#[structopt(default_value = "")]
pub project: PathBuf,
/// Authenication cookie to use. If not specified, Rojo will attempt to find one from the system automatically.
#[structopt(long)]
pub cookie: Option<String>,
/// API key obtained from create.roblox.com/credentials. Rojo will use the Open Cloud API when this is provided. Only supports uploading to a place.
#[structopt(long = "api_key")]
pub api_key: Option<String>,
/// The Universe ID of the given place. Required when using the Open Cloud API.
#[structopt(long = "universe_id")]
pub universe_id: Option<u64>,
/// Asset ID to upload to.
#[structopt(long = "asset_id")]
pub asset_id: u64,
} }
pub fn upload(options: UploadCommand) -> Result<(), anyhow::Error> { impl UploadCommand {
let cookie = options pub fn run(self) -> Result<(), anyhow::Error> {
.cookie let project_path = resolve_path(&self.project);
.clone()
.or_else(get_auth_cookie)
.ok_or(Error::NeedAuthCookie)?;
let vfs = Vfs::new_default(); let vfs = Vfs::new_default();
let session = ServeSession::new(vfs, &options.absolute_project())?; let session = ServeSession::new(vfs, project_path)?;
let tree = session.tree(); let tree = session.tree();
let inner_tree = tree.inner(); let inner_tree = tree.inner();
let root = inner_tree.root(); let root = inner_tree.root();
let encode_ids = match root.class.as_str() { let encode_ids = match root.class.as_str() {
"DataModel" => root.children().to_vec(), "DataModel" => root.children().to_vec(),
_ => vec![root.referent()], _ => vec![root.referent()],
}; };
let mut buffer = Vec::new(); let mut buffer = Vec::new();
log::trace!("Encoding binary model"); log::trace!("Encoding binary model");
rbx_binary::to_writer_default(&mut buffer, tree.inner(), &encode_ids)?; rbx_binary::to_writer(&mut buffer, tree.inner(), &encode_ids)?;
do_upload(buffer, options.asset_id, &cookie)
match (self.cookie, self.api_key, self.universe_id) {
(cookie, None, universe) => {
// using legacy. notify if universe is provided.
if universe.is_some() {
log::warn!(
"--universe_id was provided but is ignored when using legacy upload"
);
}
let cookie = cookie.or_else(get_auth_cookie).context(
"Rojo could not find your Roblox auth cookie. Please pass one via --cookie.",
)?;
do_upload(buffer, self.asset_id, &cookie)
}
(cookie, Some(api_key), Some(universe_id)) => {
// using open cloud. notify if cookie is provided.
if cookie.is_some() {
log::warn!("--cookie was provided but is ignored when using Open Cloud API");
}
do_upload_open_cloud(buffer, universe_id, self.asset_id, &api_key)
}
(_, Some(_), None) => {
// API key is provided, universe id is not.
bail!("--universe_id must be provided to use the Open Cloud API");
}
}
}
}
/// 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<()> { fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()> {
@@ -49,7 +123,7 @@ fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()>
asset_id asset_id
); );
let client = reqwest::Client::new(); let client = reqwest::blocking::Client::new();
let build_request = move || { let build_request = move || {
client client
@@ -76,10 +150,45 @@ fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()>
let status = response.status(); let status = response.status();
if !status.is_success() { if !status.is_success() {
return Err(Error::RobloxApi { bail!(
body: response.text()?, "The Roblox API returned an unexpected error: {}",
} response.text()?
.into()); );
}
Ok(())
}
/// Implementation of do_upload that supports the new open cloud api.
/// see https://developer.roblox.com/en-us/articles/open-cloud
fn do_upload_open_cloud(
buffer: Vec<u8>,
universe_id: u64,
asset_id: u64,
api_key: &str,
) -> anyhow::Result<()> {
let url = format!(
"https://apis.roblox.com/universes/v1/{}/places/{}/versions?versionType=Published",
universe_id, asset_id
);
let client = reqwest::blocking::Client::new();
log::debug!("Uploading to Roblox...");
let response = client
.post(&url)
.header("x-api-key", api_key)
.header(CONTENT_TYPE, "application/xml")
.header(ACCEPT, "application/json")
.body(buffer)
.send()?;
let status = response.status();
if !status.is_success() {
bail!(
"The Roblox API returned an unexpected error: {}",
response.text()?
);
} }
Ok(()) 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::{ use std::{
fmt::{self, Write}, fmt::{self, Write},

View File

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

View File

@@ -1,26 +1,6 @@
use std::sync::{Mutex, RwLock}; use std::sync::{Mutex, RwLock};
use futures::sync::oneshot; use futures::channel::oneshot;
struct Listener<T> {
sender: oneshot::Sender<(u32, Vec<T>)>,
cursor: u32,
}
fn fire_listener_if_ready<T: Clone>(
messages: &[T],
listener: Listener<T>,
) -> Result<(), Listener<T>> {
let current_cursor = messages.len() as u32;
if listener.cursor < current_cursor {
let new_messages = messages[(listener.cursor as usize)..].to_vec();
let _ = listener.sender.send((current_cursor, new_messages));
Ok(())
} else {
Err(listener)
}
}
/// A message queue with persistent history that can be subscribed to. /// A message queue with persistent history that can be subscribed to.
/// ///
@@ -84,6 +64,7 @@ impl<T: Clone> MessageQueue<T> {
/// This method is only useful in tests. Non-test code should use subscribe /// This method is only useful in tests. Non-test code should use subscribe
/// instead. /// instead.
#[cfg(test)] #[cfg(test)]
#[allow(unused)]
pub fn subscribe_any(&self) -> oneshot::Receiver<(u32, Vec<T>)> { pub fn subscribe_any(&self) -> oneshot::Receiver<(u32, Vec<T>)> {
let cursor = { let cursor = {
let messages = self.messages.read().unwrap(); let messages = self.messages.read().unwrap();
@@ -97,3 +78,23 @@ impl<T: Clone> MessageQueue<T> {
self.messages.read().unwrap().len() as u32 self.messages.read().unwrap().len() as u32
} }
} }
struct Listener<T> {
sender: oneshot::Sender<(u32, Vec<T>)>,
cursor: u32,
}
fn fire_listener_if_ready<T: Clone>(
messages: &[T],
listener: Listener<T>,
) -> Result<(), Listener<T>> {
let current_cursor = messages.len() as u32;
if listener.cursor < current_cursor {
let new_messages = messages[(listener.cursor as usize)..].to_vec();
let _ = listener.sender.send((current_cursor, new_messages));
Ok(())
} else {
Err(listener)
}
}

View File

@@ -36,17 +36,3 @@ where
seq.end() seq.end()
} }
pub fn serialize_option_absolute<S, T>(
maybe_path: &Option<T>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: AsRef<Path>,
{
match maybe_path {
Some(path) => serialize_absolute(path, serializer),
None => serializer.serialize_none(),
}
}

View File

@@ -1,6 +1,7 @@
use std::{ use std::{
collections::{BTreeMap, HashMap, HashSet}, collections::{BTreeMap, HashMap, HashSet},
fs, io, fs, io,
net::IpAddr,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@@ -59,12 +60,19 @@ pub struct Project {
/// If specified, sets the current place's place ID when connecting to the /// If specified, sets the current place's place ID when connecting to the
/// Rojo server from Roblox Studio. /// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
pub place_id: Option<u64>, pub place_id: Option<u64>,
/// If specified, sets the current place's game ID when connecting to the /// If specified, sets the current place's game ID when connecting to the
/// Rojo server from Roblox Studio. /// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
pub game_id: Option<u64>, pub game_id: Option<u64>,
/// If specified, this address will be used in place of the default address
/// As long as --address is unprovided.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_address: Option<IpAddr>,
/// A list of globs, relative to the folder the project file is in, that /// A list of globs, relative to the folder the project file is in, that
/// match files that should be excluded if Rojo encounters them. /// match files that should be excluded if Rojo encounters them.
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -166,6 +174,35 @@ impl Project {
} }
} }
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct OptionalPathNode {
#[serde(serialize_with = "crate::path_serializer::serialize_absolute")]
pub optional: PathBuf,
}
impl OptionalPathNode {
pub fn new(optional: PathBuf) -> Self {
OptionalPathNode { optional }
}
}
/// Describes a path that is either optional or required
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PathNode {
Required(#[serde(serialize_with = "crate::path_serializer::serialize_absolute")] PathBuf),
Optional(OptionalPathNode),
}
impl PathNode {
pub fn path(&self) -> &Path {
match self {
PathNode::Required(pathbuf) => &pathbuf,
PathNode::Optional(OptionalPathNode { optional }) => &optional,
}
}
}
/// Describes an instance and its descendants in a project. /// Describes an instance and its descendants in a project.
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ProjectNode { pub struct ProjectNode {
@@ -216,12 +253,8 @@ pub struct ProjectNode {
/// path can point to any file type supported by Rojo, including Lua files /// path can point to any file type supported by Rojo, including Lua files
/// (`.lua`), Roblox models (`.rbxm`, `.rbxmx`), and localization table /// (`.lua`), Roblox models (`.rbxm`, `.rbxmx`), and localization table
/// spreadsheets (`.csv`). /// spreadsheets (`.csv`).
#[serde( #[serde(rename = "$path", skip_serializing_if = "Option::is_none")]
rename = "$path", pub path: Option<PathNode>,
serialize_with = "crate::path_serializer::serialize_option_absolute",
skip_serializing_if = "Option::is_none"
)]
pub path: Option<PathBuf>,
} }
impl ProjectNode { impl ProjectNode {
@@ -241,3 +274,106 @@ impl ProjectNode {
} }
} }
} }
#[cfg(test)]
mod test {
use super::*;
#[test]
fn path_node_required() {
let path_node: PathNode = serde_json::from_str(r#""src""#).unwrap();
assert_eq!(path_node, PathNode::Required(PathBuf::from("src")));
}
#[test]
fn path_node_optional() {
let path_node: PathNode = serde_json::from_str(r#"{ "optional": "src" }"#).unwrap();
assert_eq!(
path_node,
PathNode::Optional(OptionalPathNode::new(PathBuf::from("src")))
);
}
#[test]
fn project_node_required() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "src"
}"#,
)
.unwrap();
assert_eq!(
project_node.path,
Some(PathNode::Required(PathBuf::from("src")))
);
}
#[test]
fn project_node_optional() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "src" }
}"#,
)
.unwrap();
assert_eq!(
project_node.path,
Some(PathNode::Optional(OptionalPathNode::new(PathBuf::from(
"src"
))))
);
}
#[test]
fn project_node_none() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$className": "Folder"
}"#,
)
.unwrap();
assert_eq!(project_node.path, None);
}
#[test]
fn project_node_optional_serialize_absolute() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "..\\src" }
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
}
#[test]
fn project_node_optional_serialize_absolute_no_change() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "../src" }
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
}
#[test]
fn project_node_optional_serialize_optional() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "..\\src"
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":"../src"}"#);
}
}

View File

@@ -1,10 +1,18 @@
use std::borrow::Borrow;
use anyhow::format_err; use anyhow::format_err;
use rbx_dom_weak::types::{ use rbx_dom_weak::types::{
Color3, Color3uint8, Content, Enum, Variant, VariantType, Vector2, Vector3, CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2, Vector3,
}; };
use rbx_reflection::{DataType, PropertyDescriptor}; use rbx_reflection::{DataType, PropertyDescriptor};
use serde::{Deserialize, Serialize}; 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)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum UnresolvedValue { pub enum UnresolvedValue {
@@ -26,10 +34,12 @@ impl UnresolvedValue {
pub enum AmbiguousValue { pub enum AmbiguousValue {
Bool(bool), Bool(bool),
String(String), String(String),
StringArray(Vec<String>),
Number(f64), Number(f64),
Array2([f64; 2]), Array2([f64; 2]),
Array3([f64; 3]), Array3([f64; 3]),
Array4([f64; 4]), Array4([f64; 4]),
Array12([f64; 12]),
} }
impl AmbiguousValue { impl AmbiguousValue {
@@ -46,13 +56,14 @@ impl AmbiguousValue {
})?; })?;
let error = |what: &str| { let error = |what: &str| {
let sample_values = enum_descriptor let mut all_values = enum_descriptor
.items .items
.keys() .keys()
.take(3) .map(|value| value.borrow())
.map(|name| format!(r#""{}""#, name)) .collect::<Vec<_>>();
.collect::<Vec<_>>() all_values.sort();
.join(", ");
let examples = nonexhaustive_list(&all_values);
format_err!( format_err!(
"Invalid value for property {}.{}. Got {} but \ "Invalid value for property {}.{}. Got {} but \
@@ -61,7 +72,7 @@ impl AmbiguousValue {
prop_name, prop_name,
what, what,
enum_name, enum_name,
sample_values examples,
) )
}; };
@@ -86,6 +97,9 @@ impl AmbiguousValue {
(VariantType::Int64, AmbiguousValue::Number(value)) => Ok((value as i64).into()), (VariantType::Int64, AmbiguousValue::Number(value)) => Ok((value as i64).into()),
(VariantType::String, AmbiguousValue::String(value)) => Ok(value.into()), (VariantType::String, AmbiguousValue::String(value)) => Ok(value.into()),
(VariantType::Tags, AmbiguousValue::StringArray(value)) => {
Ok(Tags::from(value).into())
}
(VariantType::Content, AmbiguousValue::String(value)) => { (VariantType::Content, AmbiguousValue::String(value)) => {
Ok(Content::from(value).into()) Ok(Content::from(value).into())
} }
@@ -101,14 +115,17 @@ impl AmbiguousValue {
(VariantType::Color3, AmbiguousValue::Array3(value)) => { (VariantType::Color3, AmbiguousValue::Array3(value)) => {
Ok(Color3::new(value[0] as f32, value[1] as f32, value[2] as f32).into()) 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( (VariantType::CFrame, AmbiguousValue::Array12(value)) => {
(value[0] / 255.0) as u8, let value = value.map(|v| v as f32);
(value[1] / 255.0) as u8, let pos = Vector3::new(value[0], value[1], value[2]);
(value[2] / 255.0) as u8, let orientation = Matrix3::new(
Vector3::new(value[3], value[4], value[5]),
Vector3::new(value[6], value[7], value[8]),
Vector3::new(value[9], value[10], value[11]),
); );
Ok(value.into()) Ok(CFrame::new(pos, orientation).into())
} }
(_, unresolved) => Err(format_err!( (_, unresolved) => Err(format_err!(
@@ -131,10 +148,12 @@ impl AmbiguousValue {
match self { match self {
AmbiguousValue::Bool(_) => "a bool", AmbiguousValue::Bool(_) => "a bool",
AmbiguousValue::String(_) => "a string", AmbiguousValue::String(_) => "a string",
AmbiguousValue::StringArray(_) => "an array of strings",
AmbiguousValue::Number(_) => "a number", AmbiguousValue::Number(_) => "a number",
AmbiguousValue::Array2(_) => "an array of two numbers", AmbiguousValue::Array2(_) => "an array of two numbers",
AmbiguousValue::Array3(_) => "an array of three numbers", AmbiguousValue::Array3(_) => "an array of three numbers",
AmbiguousValue::Array4(_) => "an array of four numbers", AmbiguousValue::Array4(_) => "an array of four numbers",
AmbiguousValue::Array12(_) => "an array of twelve numbers",
} }
} }
} }
@@ -155,3 +174,121 @@ fn find_descriptor(
current_class_name = class.superclass.as_deref()?; 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

@@ -2,6 +2,7 @@ use std::{
borrow::Cow, borrow::Cow,
collections::HashSet, collections::HashSet,
io, io,
net::IpAddr,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, Mutex, MutexGuard}, sync::{Arc, Mutex, MutexGuard},
time::Instant, time::Instant,
@@ -24,7 +25,9 @@ use crate::{
snapshot_middleware::snapshot_from_vfs, snapshot_middleware::snapshot_from_vfs,
}; };
/// Contains all of the state for a Rojo serve session. /// Contains all of the state for a Rojo serve session. A serve session is used
/// when we need to build a Rojo tree and possibly rebuild it when input files
/// change.
/// ///
/// Nothing here is specific to any Rojo interface. Though the primary way to /// Nothing here is specific to any Rojo interface. Though the primary way to
/// interact with a serve session is Rojo's HTTP right now, there's no reason /// interact with a serve session is Rojo's HTTP right now, there's no reason
@@ -124,11 +127,10 @@ impl ServeSession {
let instance_context = InstanceContext::default(); let instance_context = InstanceContext::default();
log::trace!("Generating snapshot of instances from VFS"); log::trace!("Generating snapshot of instances from VFS");
let snapshot = snapshot_from_vfs(&instance_context, &vfs, &start_path)? let snapshot = snapshot_from_vfs(&instance_context, &vfs, &start_path)?;
.expect("snapshot did not return an instance");
log::trace!("Computing initial patch set"); log::trace!("Computing initial patch set");
let patch_set = compute_patch_set(&snapshot, &tree, root_id); let patch_set = compute_patch_set(snapshot.as_ref(), &tree, root_id);
log::trace!("Applying initial patch set"); log::trace!("Applying initial patch set");
apply_patch_set(&mut tree, patch_set); apply_patch_set(&mut tree, patch_set);
@@ -210,6 +212,14 @@ impl ServeSession {
pub fn serve_place_ids(&self) -> Option<&HashSet<u64>> { pub fn serve_place_ids(&self) -> Option<&HashSet<u64>> {
self.root_project.serve_place_ids.as_ref() self.root_project.serve_place_ids.as_ref()
} }
pub fn serve_address(&self) -> Option<IpAddr> {
self.root_project.serve_address
}
pub fn root_dir(&self) -> &Path {
self.root_project.folder_location()
}
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]

View File

@@ -10,16 +10,27 @@ use super::{
InstanceSnapshot, InstanceWithMeta, RojoTree, InstanceSnapshot, InstanceWithMeta, RojoTree,
}; };
pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: Ref) -> PatchSet { pub fn compute_patch_set(
snapshot: Option<&InstanceSnapshot>,
tree: &RojoTree,
id: Ref,
) -> PatchSet {
let mut patch_set = PatchSet::new(); let mut patch_set = PatchSet::new();
let mut context = ComputePatchContext::default();
compute_patch_set_internal(&mut context, snapshot, tree, id, &mut patch_set); if let Some(snapshot) = snapshot {
let mut context = ComputePatchContext::default();
// Rewrite Ref properties to refer to instance IDs instead of snapshot IDs compute_patch_set_internal(&mut context, snapshot, tree, id, &mut patch_set);
// for all of the IDs that we know about so far.
rewrite_refs_in_updates(&context, &mut patch_set.updated_instances); // Rewrite Ref properties to refer to instance IDs instead of snapshot IDs
rewrite_refs_in_additions(&context, &mut patch_set.added_instances); // for all of the IDs that we know about so far.
rewrite_refs_in_updates(&context, &mut patch_set.updated_instances);
rewrite_refs_in_additions(&context, &mut patch_set.added_instances);
} else {
if id != tree.get_root_id() {
patch_set.removed_instances.push(id);
}
}
patch_set patch_set
} }
@@ -246,7 +257,7 @@ mod test {
children: Vec::new(), children: Vec::new(),
}; };
let patch_set = compute_patch_set(&snapshot, &tree, root_id); let patch_set = compute_patch_set(Some(&snapshot), &tree, root_id);
let expected_patch_set = PatchSet { let expected_patch_set = PatchSet {
updated_instances: vec![PatchUpdate { updated_instances: vec![PatchUpdate {
@@ -296,7 +307,7 @@ mod test {
class_name: Cow::Borrowed("foo"), class_name: Cow::Borrowed("foo"),
}; };
let patch_set = compute_patch_set(&snapshot, &tree, root_id); let patch_set = compute_patch_set(Some(&snapshot), &tree, root_id);
let expected_patch_set = PatchSet { let expected_patch_set = PatchSet {
added_instances: vec![PatchAdd { added_instances: vec![PatchAdd {

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