Compare commits

...

47 Commits

Author SHA1 Message Date
Lucien Greathouse
88d2d1f193 Stub out a command sorta like rostar unpack 2022-06-10 03:15:44 -04:00
Lucien Greathouse
2e7c4b6dff Update Changelog 2022-06-10 02:15:07 -04:00
Lucien Greathouse
e5dbee1073 Defer application of init.meta.json until after init Lua files. (#549)
Fixes #546.
2022-06-09 21:42:37 -04:00
Lucien Greathouse
c06463b61d Update CHANGELOG 2022-06-05 17:48:17 -04:00
Lucien Greathouse
10341e3776 Major Performance Improvements (#548)
* Use WeakDom::into_raw for faster snapshot generation from models

* Make compute_patch_set take snapshots by value

* Stop deferring property application in apply_patch_set

* Use InstanceBuilder::empty to avoid extra name allocations

* Git dependencies, skip dropping ServeSession

* Use std::mem::forget instead of ManuallyDrop

* Switch to latest rbx-dom crates.io dependencies

* Update other dependencies
2022-06-05 17:47:31 -04:00
Lucien Greathouse
824cdc5dcd Annotate snapshot_rbxm for profiling 2022-05-27 18:13:07 -04:00
Lucien Greathouse
7aa7a35aa5 Add profiling info and optional profiling with Tracy 2022-05-27 03:08:54 -04:00
Lucien Greathouse
79b57b3359 Move memofs and rojo-insta-ext into crates folder 2022-05-26 04:23:44 -04:00
Lucien Greathouse
c7aeffe586 Switch from structopt to clap 2022-05-26 04:19:51 -04:00
Lucien Greathouse
79c02f2457 Delete old bin folder and update foreman.toml 2022-05-26 04:13:50 -04:00
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
106 changed files with 3900 additions and 1934 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.46.0"] 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

@@ -1,6 +1,48 @@
# Rojo Changelog # Rojo Changelog
## Unreleased Changes ## Unreleased Changes
* Switched from structopt to clap for command line argument parsing.
* Significantly improved performance of building and serving. ([#548])
* Fixed `init.meta.json` when used with `init.lua` and related files. ([#549])
[#548]: https://github.com/rojo-rbx/rojo/pull/548
[#549]: https://github.com/rojo-rbx/rojo/pull/549
## [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 ## [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. 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.
@@ -75,7 +117,7 @@ The shorthand property format that most users use is not impacted. For reference
## [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,6 +29,11 @@ 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:
@@ -44,11 +49,9 @@ The Rojo release process is pretty manual right now. If you need to do it, here'
* `cargo publish` * `cargo publish`
8. Publish the Plugin 8. Publish the Plugin
* `cargo run -- upload plugin --asset_id 6415005344` * `cargo run -- upload plugin --asset_id 6415005344`
* `cargo run -- build plugin --output 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

1912
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-rc.3" 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"
@@ -27,11 +27,10 @@ default = []
# Enable this feature to live-reload assets from the web UI. # Enable this feature to live-reload assets from the web UI.
dev_live_assets = [] dev_live_assets = []
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client"]
[workspace] [workspace]
members = [ members = ["crates/*"]
"rojo-insta-ext",
"memofs",
]
[lib] [lib]
name = "librojo" name = "librojo"
@@ -42,7 +41,7 @@ name = "build"
harness = false harness = false
[dependencies] [dependencies]
memofs = { version = "0.2.0", path = "memofs" } memofs = { version = "0.2.0", path = "crates/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" }
@@ -52,7 +51,7 @@ memofs = { version = "0.2.0", path = "memofs" }
# rbx_xml = { path = "../rbx-dom/rbx_xml" } # rbx_xml = { path = "../rbx-dom/rbx_xml" }
rbx_binary = "0.6.4" rbx_binary = "0.6.4"
rbx_dom_weak = "2.3.0" rbx_dom_weak = "2.4.0"
rbx_reflection = "4.2.0" rbx_reflection = "4.2.0"
rbx_reflection_database = "0.2.2" rbx_reflection_database = "0.2.2"
rbx_xml = "0.12.3" rbx_xml = "0.12.3"
@@ -69,29 +68,28 @@ globset = "0.4.8"
humantime = "2.1.0" humantime = "2.1.0"
hyper = { version = "0.14.13", features = ["server", "tcp", "http1"] } hyper = { version = "0.14.13", features = ["server", "tcp", "http1"] }
jod-thread = "0.1.2" jod-thread = "0.1.2"
lazy_static = "1.4.0"
log = "0.4.14" log = "0.4.14"
maplit = "1.0.2" maplit = "1.0.2"
notify = "4.0.17" notify = "4.0.17"
opener = "0.5.0" opener = "0.5.0"
regex = "1.5.4" reqwest = { version = "0.11.10", features = ["blocking", "json"] }
reqwest = "0.9.24"
ritz = "0.1.0" ritz = "0.1.0"
rlua = "0.17.1"
roblox_install = "1.0.0" roblox_install = "1.0.0"
serde = { version = "1.0.130", features = ["derive", "rc"] } serde = { version = "1.0.130", features = ["derive", "rc"] }
serde_json = "1.0.68" serde_json = "1.0.68"
structopt = "0.3.23"
termcolor = "1.1.2" termcolor = "1.1.2"
thiserror = "1.0.30" thiserror = "1.0.30"
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] } tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
uuid = { version = "0.8.2", features = ["v4", "serde"] } uuid = { version = "1.0.0", features = ["v4", "serde"] }
clap = { version = "3.1.18", features = ["derive"] }
profiling = "1.0.6"
tracy-client = { version = "0.13.2", optional = true }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winreg = "0.9.0" winreg = "0.10.1"
[build-dependencies] [build-dependencies]
memofs = { version = "0.2.0", path = "memofs" } memofs = { version = "0.2.0", path = "crates/memofs" }
embed-resource = "1.6.4" embed-resource = "1.6.4"
anyhow = "1.0.44" anyhow = "1.0.44"
@@ -100,13 +98,12 @@ fs-err = "2.6.0"
maplit = "1.0.2" maplit = "1.0.2"
[dev-dependencies] [dev-dependencies]
rojo-insta-ext = { path = "rojo-insta-ext" } rojo-insta-ext = { path = "crates/rojo-insta-ext" }
criterion = "0.3.5" criterion = "0.3.5"
insta = { version = "1.8.0", features = ["redactions"] } insta = { version = "1.8.0", features = ["redactions"] }
lazy_static = "1.4.0"
paste = "1.0.5" paste = "1.0.5"
pretty_assertions = "0.7.2" pretty_assertions = "1.2.1"
serde_yaml = "0.8.21" serde_yaml = "0.8.21"
tempfile = "3.2.0" tempfile = "3.2.0"
walkdir = "2.3.2" walkdir = "2.3.2"

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

@@ -1,5 +0,0 @@
#!/bin/sh
set -e
watchexec -c -w plugin "sh -c './bin/install-dev-plugin.sh'"

View File

@@ -1,13 +0,0 @@
#!/bin/sh
set -e
DIR="$( mktemp -d )"
PLUGIN_FILE="$DIR/Rojo.rbxm"
TESTEZ_FILE="$DIR/TestEZ.rbxm"
rojo build plugin -o "$PLUGIN_FILE"
rojo build plugin/testez.project.json -o "$TESTEZ_FILE"
remodel bin/mark-plugin-as-dev.lua "$PLUGIN_FILE" "$TESTEZ_FILE" 2>/dev/null
cp "$PLUGIN_FILE" "$LOCALAPPDATA/Roblox/Plugins/Rojo.rbxm"

View File

@@ -1,5 +0,0 @@
#!/bin/sh
set -e
rojo build plugin -o "$LOCALAPPDATA/Roblox/Plugins/Rojo.rbxm"

View File

@@ -1,12 +0,0 @@
local pluginPath, testezPath = ...
local plugin = remodel.readModelFile(pluginPath)[1]
local testez = remodel.readModelFile(testezPath)[1]
local marker = Instance.new("Folder")
marker.Name = "ROJO_DEV_BUILD"
marker.Parent = plugin
testez.Parent = plugin
remodel.writeModelFile(plugin, pluginPath)

View File

@@ -1,8 +0,0 @@
local pluginPath, placePath = ...
local plugin = remodel.readModelFile(pluginPath)[1]
local place = remodel.readPlaceFile(placePath)
plugin.Parent = place:GetService("ReplicatedStorage")
remodel.writePlaceFile(place, placePath)

View File

@@ -1,6 +0,0 @@
#!/bin/sh
set -e
./bin/run-cli-tests.sh
./bin/run-plugin-tests.sh

View File

@@ -1,9 +0,0 @@
#!/bin/sh
set -e
cargo test --all --locked
cargo fmt -- --check
touch src/lib.rs # Nudge Rust source to make Clippy actually check things
cargo clippy

View File

@@ -1,16 +0,0 @@
#!/bin/sh
set -e
DIR="$( mktemp -d )"
PLUGIN_FILE="$DIR/Rojo.rbxmx"
PLACE_FILE="$DIR/RojoTestPlace.rbxlx"
rojo build plugin -o "$PLUGIN_FILE"
rojo build plugin/place.project.json -o "$PLACE_FILE"
remodel bin/put-plugin-in-test-place.lua "$PLUGIN_FILE" "$PLACE_FILE"
run-in-roblox -s plugin/testBootstrap.server.lua "$PLACE_FILE"
luacheck plugin/src plugin/log plugin/http

View File

@@ -1,3 +1,4 @@
[tools] [tools]
rojo = { source = "rojo-rbx/rojo", version = "6.1.0" } rojo = { source = "rojo-rbx/rojo", version = "7.1.1" }
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" } run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }
selene = { source = "Kampfkarren/selene", version = "0.17.0" }

File diff suppressed because it is too large Load Diff

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

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

@@ -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, "-rc.3"}, version = {7, 1, 1},
expectedServerVersionString = "7.0 or newer", expectedServerVersionString = "7.0 or newer",
protocolVersion = 4, protocolVersion = 4,
defaultHost = "localhost", defaultHost = "localhost",

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

View File

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

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

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

View File

@@ -0,0 +1,14 @@
---
source: tests/tests/build.rs
assertion_line: 98
expression: contents
---
<roblox version="4">
<Item class="LocalScript" referent="0">
<Properties>
<string name="Name">issue_546</string>
<bool name="Disabled">true</bool>
<string name="Source">print("Hello, world!")</string>
</Properties>
</Item>
</roblox>

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

@@ -32,6 +32,20 @@ expression: contents
<Item class="Part" referent="4"> <Item class="Part" referent="4">
<Properties> <Properties>
<string name="Name">Color</string> <string name="Name">Color</string>
<CoordinateFrame name="CFrame">
<X>1</X>
<Y>2</Y>
<Z>3</Z>
<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> <Color3uint8 name="Color3uint8">8404992</Color3uint8>
</Properties> </Properties>
</Item> </Item>

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,2 @@
# Issue #546 (https://github.com/rojo-rbx/rojo/issues/546)
Regression from Rojo 6.2.0 to Rojo 7.0.0. Meta files named as init.meta.json should apply after init.client.lua and other init files.

View File

@@ -0,0 +1,6 @@
{
"name": "issue_546",
"tree": {
"$path": "hello"
}
}

View File

@@ -0,0 +1 @@
print("Hello, world!")

View File

@@ -0,0 +1,5 @@
{
"properties": {
"Disabled": true
}
}

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

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

@@ -284,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, &tree, id);
apply_patch_set(tree, patch_set) apply_patch_set(tree, patch_set)
} }
Ok(None) => { Ok(None) => {
@@ -335,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, &tree, id);
apply_patch_set(tree, patch_set) apply_patch_set(tree, patch_set)
} }
}; };

View File

@@ -1,12 +1,13 @@
use std::{ use std::{
io::{BufWriter, Write}, io::{BufWriter, Write},
mem::forget,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use anyhow::Context; use anyhow::Context;
use clap::Parser;
use fs_err::File; use fs_err::File;
use memofs::Vfs; use memofs::Vfs;
use structopt::StructOpt;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use crate::serve_session::ServeSession; use crate::serve_session::ServeSession;
@@ -17,20 +18,20 @@ const UNKNOWN_OUTPUT_KIND_ERR: &str = "Could not detect what kind of file to bui
Expected output file to end in .rbxl, .rbxlx, .rbxm, or .rbxmx."; Expected output file to end in .rbxl, .rbxlx, .rbxm, or .rbxmx.";
/// Generates a model or place file from the Rojo project. /// Generates a model or place file from the Rojo project.
#[derive(Debug, StructOpt)] #[derive(Debug, Parser)]
pub struct BuildCommand { pub struct BuildCommand {
/// Path to the project to serve. Defaults to the current directory. /// Path to the project to serve. Defaults to the current directory.
#[structopt(default_value = "")] #[clap(default_value = "")]
pub project: PathBuf, pub project: PathBuf,
/// Where to output the result. /// Where to output the result.
/// ///
/// Should end in .rbxm, .rbxl, .rbxmx, or .rbxlx. /// Should end in .rbxm, .rbxl, .rbxmx, or .rbxlx.
#[structopt(long, short)] #[clap(long, short)]
pub output: PathBuf, pub output: PathBuf,
/// Whether to automatically rebuild when any input files change. /// Whether to automatically rebuild when any input files change.
#[structopt(long)] #[clap(long)]
pub watch: bool, pub watch: bool,
} }
@@ -61,6 +62,10 @@ impl BuildCommand {
} }
} }
// Avoid dropping ServeSession: it's potentially VERY expensive to drop
// and we're about to exit anyways.
forget(session);
Ok(()) Ok(())
} }
} }
@@ -97,6 +102,7 @@ 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)
} }
#[profiling::function]
fn write_model( fn write_model(
session: &ServeSession, session: &ServeSession,
output: &Path, output: &Path,

View File

@@ -1,7 +1,7 @@
use structopt::StructOpt; use clap::Parser;
/// Open Rojo's documentation in your browser. /// Open Rojo's documentation in your browser.
#[derive(Debug, StructOpt)] #[derive(Debug, Parser)]
pub struct DocCommand {} pub struct DocCommand {}
impl DocCommand { impl DocCommand {

View File

@@ -1,15 +1,15 @@
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::Context; use anyhow::Context;
use structopt::StructOpt; use clap::Parser;
use crate::project::Project; use crate::project::Project;
/// Reformat a Rojo project using the standard JSON formatting rules. /// Reformat a Rojo project using the standard JSON formatting rules.
#[derive(Debug, StructOpt)] #[derive(Debug, Parser)]
pub struct FmtProjectCommand { pub struct FmtProjectCommand {
/// Path to the project to format. Defaults to the current directory. /// Path to the project to format. Defaults to the current directory.
#[structopt(default_value = "")] #[clap(default_value = "")]
pub project: PathBuf, pub project: PathBuf,
} }

View File

@@ -4,9 +4,9 @@ use std::process::{Command, Stdio};
use std::str::FromStr; use std::str::FromStr;
use anyhow::{bail, format_err}; use anyhow::{bail, format_err};
use clap::Parser;
use fs_err as fs; use fs_err as fs;
use fs_err::OpenOptions; use fs_err::OpenOptions;
use structopt::StructOpt;
use super::resolve_path; use super::resolve_path;
@@ -22,14 +22,14 @@ static PLACE_README: &str = include_str!("../../assets/default-place-project/REA
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");
/// Initializes a new Rojo project. /// Initializes a new Rojo project.
#[derive(Debug, StructOpt)] #[derive(Debug, Parser)]
pub struct InitCommand { pub struct InitCommand {
/// Path to the place to create the project. Defaults to the current directory. /// Path to the place to create the project. Defaults to the current directory.
#[structopt(default_value = "")] #[clap(default_value = "")]
pub path: PathBuf, pub path: PathBuf,
/// The kind of project to create, 'place' or 'model'. Defaults to place. /// The kind of project to create, 'place' or 'model'. Defaults to place.
#[structopt(long, default_value = "place")] #[clap(long, default_value = "place")]
pub kind: InitKind, pub kind: InitKind,
} }

View File

@@ -1,4 +1,4 @@
//! Defines Rojo's CLI through structopt types. //! Defines Rojo's CLI through clap types.
mod build; mod build;
mod doc; mod doc;
@@ -6,11 +6,13 @@ mod fmt_project;
mod init; mod init;
mod plugin; mod plugin;
mod serve; mod serve;
mod sourcemap;
mod unpack;
mod upload; mod upload;
use std::{borrow::Cow, env, path::Path, str::FromStr}; use std::{borrow::Cow, env, path::Path, str::FromStr};
use structopt::StructOpt; use clap::Parser;
use thiserror::Error; use thiserror::Error;
pub use self::build::BuildCommand; pub use self::build::BuildCommand;
@@ -19,17 +21,19 @@ pub use self::fmt_project::FmtProjectCommand;
pub use self::init::{InitCommand, InitKind}; pub use self::init::{InitCommand, InitKind};
pub use self::plugin::{PluginCommand, PluginSubcommand}; pub use self::plugin::{PluginCommand, PluginSubcommand};
pub use self::serve::ServeCommand; pub use self::serve::ServeCommand;
pub use self::sourcemap::SourcemapCommand;
pub use self::unpack::UnpackCommand;
pub use self::upload::UploadCommand; 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 clap crate.
#[derive(Debug, StructOpt)] #[derive(Debug, Parser)]
#[structopt(name = "Rojo", about, author)] #[clap(name = "Rojo", version, about, author)]
pub struct Options { pub struct Options {
#[structopt(flatten)] #[clap(flatten)]
pub global: GlobalOptions, pub global: GlobalOptions,
/// Subcommand to run in this invocation. /// Subcommand to run in this invocation.
#[structopt(subcommand)] #[clap(subcommand)]
pub subcommand: Subcommand, pub subcommand: Subcommand,
} }
@@ -40,21 +44,23 @@ impl Options {
Subcommand::Serve(subcommand) => subcommand.run(self.global), Subcommand::Serve(subcommand) => subcommand.run(self.global),
Subcommand::Build(subcommand) => subcommand.run(), Subcommand::Build(subcommand) => subcommand.run(),
Subcommand::Upload(subcommand) => subcommand.run(), Subcommand::Upload(subcommand) => subcommand.run(),
Subcommand::Sourcemap(subcommand) => subcommand.run(),
Subcommand::FmtProject(subcommand) => subcommand.run(), Subcommand::FmtProject(subcommand) => subcommand.run(),
Subcommand::Doc(subcommand) => subcommand.run(), Subcommand::Doc(subcommand) => subcommand.run(),
Subcommand::Plugin(subcommand) => subcommand.run(), Subcommand::Plugin(subcommand) => subcommand.run(),
Subcommand::Unpack(subcommand) => subcommand.run(),
} }
} }
} }
#[derive(Debug, StructOpt)] #[derive(Debug, Parser)]
pub struct GlobalOptions { pub struct GlobalOptions {
/// Sets verbosity level. Can be specified multiple times. /// Sets verbosity level. Can be specified multiple times.
#[structopt(long("verbose"), short, global(true), parse(from_occurrences))] #[clap(long("verbose"), short, global(true), parse(from_occurrences))]
pub verbosity: u8, pub verbosity: u8,
/// Set color behavior. Valid values are auto, always, and never. /// Set color behavior. Valid values are auto, always, and never.
#[structopt(long("color"), global(true), default_value("auto"))] #[clap(long("color"), global(true), default_value("auto"))]
pub color: ColorChoice, pub color: ColorChoice,
} }
@@ -106,15 +112,17 @@ pub struct ColorChoiceParseError {
attempted: String, attempted: String,
} }
#[derive(Debug, StructOpt)] #[derive(Debug, Parser)]
pub enum Subcommand { pub enum Subcommand {
Init(InitCommand), Init(InitCommand),
Serve(ServeCommand), Serve(ServeCommand),
Build(BuildCommand), Build(BuildCommand),
Upload(UploadCommand), Upload(UploadCommand),
Sourcemap(SourcemapCommand),
FmtProject(FmtProjectCommand), FmtProject(FmtProjectCommand),
Doc(DocCommand), Doc(DocCommand),
Plugin(PluginCommand), Plugin(PluginCommand),
Unpack(UnpackCommand),
} }
pub(super) fn resolve_path(path: &Path) -> Cow<'_, Path> { pub(super) fn resolve_path(path: &Path) -> Cow<'_, Path> {

View File

@@ -3,9 +3,9 @@ use std::{
io::BufWriter, io::BufWriter,
}; };
use clap::Parser;
use memofs::{InMemoryFs, Vfs, VfsSnapshot}; use memofs::{InMemoryFs, Vfs, VfsSnapshot};
use roblox_install::RobloxStudio; use roblox_install::RobloxStudio;
use structopt::StructOpt;
use crate::serve_session::ServeSession; use crate::serve_session::ServeSession;
@@ -13,14 +13,14 @@ static PLUGIN_BINCODE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/plugin.
static PLUGIN_FILE_NAME: &str = "RojoManagedPlugin.rbxm"; static PLUGIN_FILE_NAME: &str = "RojoManagedPlugin.rbxm";
/// Install Rojo's plugin. /// Install Rojo's plugin.
#[derive(Debug, StructOpt)] #[derive(Debug, Parser)]
pub struct PluginCommand { pub struct PluginCommand {
#[structopt(subcommand)] #[clap(subcommand)]
subcommand: PluginSubcommand, subcommand: PluginSubcommand,
} }
/// Manages Rojo's Roblox Studio plugin. /// Manages Rojo's Roblox Studio plugin.
#[derive(Debug, StructOpt)] #[derive(Debug, Parser)]
pub enum PluginSubcommand { pub enum PluginSubcommand {
/// Install the plugin in Roblox Studio's plugins folder. If the plugin is /// Install the plugin in Roblox Studio's plugins folder. If the plugin is
/// already installed, installing it again will overwrite the current plugin /// already installed, installing it again will overwrite the current plugin

View File

@@ -5,8 +5,8 @@ use std::{
sync::Arc, sync::Arc,
}; };
use clap::Parser;
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::{serve_session::ServeSession, web::LiveServer}; use crate::{serve_session::ServeSession, web::LiveServer};
@@ -17,19 +17,19 @@ const DEFAULT_BIND_ADDRESS: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
const DEFAULT_PORT: u16 = 34872; const DEFAULT_PORT: u16 = 34872;
/// Expose a Rojo project to the Rojo Studio plugin. /// Expose a Rojo project to the Rojo Studio plugin.
#[derive(Debug, StructOpt)] #[derive(Debug, Parser)]
pub struct ServeCommand { pub struct ServeCommand {
/// Path to the project to serve. Defaults to the current directory. /// Path to the project to serve. Defaults to the current directory.
#[structopt(default_value = "")] #[clap(default_value = "")]
pub project: PathBuf, pub project: PathBuf,
/// The IP address to listen on. Defaults to `127.0.0.1`. /// The IP address to listen on. Defaults to `127.0.0.1`.
#[structopt(long)] #[clap(long)]
pub address: Option<IpAddr>, pub address: Option<IpAddr>,
/// The port to listen on. Defaults to the project's preference, or `34872` if /// The port to listen on. Defaults to the project's preference, or `34872` if
/// it has none. /// it has none.
#[structopt(long)] #[clap(long)]
pub port: Option<u16>, pub port: Option<u16>,
} }
@@ -41,7 +41,10 @@ impl ServeCommand {
let session = Arc::new(ServeSession::new(vfs, &project_path)?); let session = Arc::new(ServeSession::new(vfs, &project_path)?);
let ip = self.address.unwrap_or(DEFAULT_BIND_ADDRESS.into()); let ip = self
.address
.or_else(|| session.serve_address())
.unwrap_or(DEFAULT_BIND_ADDRESS.into());
let port = self let port = self
.port .port

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

@@ -0,0 +1,136 @@
use std::{
io::{BufWriter, Write},
path::{Path, PathBuf},
};
use clap::Parser;
use fs_err::File;
use memofs::Vfs;
use rbx_dom_weak::types::Ref;
use serde::Serialize;
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, Parser)]
pub struct SourcemapCommand {
/// Path to the project to use for the sourcemap. Defaults to the current
/// directory.
#[clap(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.
#[clap(long, short)]
pub output: Option<PathBuf>,
/// If non-script files should be included or not. Defaults to false.
#[clap(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,
})
}

65
src/cli/unpack.rs Normal file
View File

@@ -0,0 +1,65 @@
use std::{io::BufReader, path::PathBuf};
use anyhow::bail;
use clap::Parser;
use fs_err::File;
use rbx_dom_weak::{Instance, WeakDom};
use crate::{Project, ProjectNode};
use super::resolve_path;
/// Unpack a Roblox place file into an existing Rojo project.
#[derive(Debug, Parser)]
pub struct UnpackCommand {
/// Path to the project to unpack. Defaults to the current directory.
#[clap(long, default_value = "")]
pub project: PathBuf,
/// Path to the place to unpack from.
pub place: PathBuf,
}
impl UnpackCommand {
pub fn run(self) -> anyhow::Result<()> {
let project_path = resolve_path(&self.project);
let project = match Project::load_fuzzy(&project_path)? {
Some(project) => project,
None => bail!("No project file was found; rojo unpack requires a project file."),
};
let place_ext = self
.place
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_lowercase());
let file = BufReader::new(File::open(&self.place)?);
let dom = match place_ext.as_deref() {
Some("rbxl") => rbx_binary::from_reader(file)?,
Some("rbxlx") => rbx_xml::from_reader_default(file)?,
Some(_) | None => bail!("Place files must end in .rbxl or .rbxlx"),
};
let context = Context { project, dom };
context.unpack();
Ok(())
}
}
struct Context {
project: Project,
dom: WeakDom,
}
impl Context {
fn unpack(&self) {
self.unpack_node(&self.project.tree, self.dom.root());
}
fn unpack_node(&self, node: &ProjectNode, instance: &Instance) {
// TODO
}
}

View File

@@ -2,30 +2,38 @@ use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use anyhow::{bail, format_err, Context}; use anyhow::{bail, format_err, Context};
use clap::Parser;
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 structopt::StructOpt;
use crate::{auth_cookie::get_auth_cookie, serve_session::ServeSession}; use crate::{auth_cookie::get_auth_cookie, serve_session::ServeSession};
use super::resolve_path; use super::resolve_path;
/// Builds the project and uploads it to Roblox. /// Builds the project and uploads it to Roblox.
#[derive(Debug, StructOpt)] #[derive(Debug, Parser)]
pub struct UploadCommand { pub struct UploadCommand {
/// Path to the project to upload. Defaults to the current directory. /// Path to the project to upload. Defaults to the current directory.
#[structopt(default_value = "")] #[clap(default_value = "")]
pub project: PathBuf, pub project: PathBuf,
/// Authenication cookie to use. If not specified, Rojo will attempt to find one from the system automatically. /// Authenication cookie to use. If not specified, Rojo will attempt to find one from the system automatically.
#[structopt(long)] #[clap(long)]
pub cookie: Option<String>, 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.
#[clap(long = "api_key")]
pub api_key: Option<String>,
/// The Universe ID of the given place. Required when using the Open Cloud API.
#[clap(long = "universe_id")]
pub universe_id: Option<u64>,
/// Asset ID to upload to. /// Asset ID to upload to.
#[structopt(long = "asset_id")] #[clap(long = "asset_id")]
pub asset_id: u64, pub asset_id: u64,
} }
@@ -33,10 +41,6 @@ impl UploadCommand {
pub fn run(self) -> Result<(), anyhow::Error> { pub fn run(self) -> Result<(), anyhow::Error> {
let project_path = resolve_path(&self.project); let project_path = resolve_path(&self.project);
let cookie = self.cookie.or_else(get_auth_cookie).context(
"Rojo could not find your Roblox auth cookie. Please pass one via --cookie.",
)?;
let vfs = Vfs::new_default(); let vfs = Vfs::new_default();
let session = ServeSession::new(vfs, project_path)?; let session = ServeSession::new(vfs, project_path)?;
@@ -54,7 +58,36 @@ impl UploadCommand {
log::trace!("Encoding binary model"); log::trace!("Encoding binary model");
rbx_binary::to_writer(&mut buffer, tree.inner(), &encode_ids)?; rbx_binary::to_writer(&mut buffer, tree.inner(), &encode_ids)?;
do_upload(buffer, self.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");
}
}
} }
} }
@@ -90,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
@@ -125,3 +158,38 @@ fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()>
Ok(()) 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(())
}

View File

@@ -1,11 +1,14 @@
use std::{env, panic, process}; use std::{env, panic, process};
use backtrace::Backtrace; use backtrace::Backtrace;
use structopt::StructOpt; use clap::Parser;
use librojo::cli::Options; use librojo::cli::Options;
fn main() { fn main() {
#[cfg(feature = "profile-with-tracy")]
tracy_client::Client::start();
panic::set_hook(Box::new(|panic_info| { panic::set_hook(Box::new(|panic_info| {
// PanicInfo's payload is usually a &'static str or String. // PanicInfo's payload is usually a &'static str or String.
// See: https://doc.rust-lang.org/beta/std/panic/struct.PanicInfo.html#method.payload // See: https://doc.rust-lang.org/beta/std/panic/struct.PanicInfo.html#method.payload
@@ -49,7 +52,7 @@ fn main() {
process::exit(1); process::exit(1);
})); }));
let options = Options::from_args(); let options = Options::parse();
let log_filter = match options.global.verbosity { let log_filter = match options.global.verbosity {
0 => "info", 0 => "info",

View File

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

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},
}; };
@@ -67,6 +68,11 @@ pub struct Project {
#[serde(skip_serializing_if = "Option::is_none")] #[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")]
@@ -168,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 {
@@ -218,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 {
@@ -243,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,7 +1,9 @@
use std::borrow::Borrow; use std::borrow::Borrow;
use anyhow::format_err; use anyhow::format_err;
use rbx_dom_weak::types::{Color3, Content, Enum, Tags, Variant, VariantType, Vector2, Vector3}; use rbx_dom_weak::types::{
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};
@@ -37,6 +39,7 @@ pub enum AmbiguousValue {
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 {
@@ -113,6 +116,18 @@ impl AmbiguousValue {
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::CFrame, AmbiguousValue::Array12(value)) => {
let value = value.map(|v| v as f32);
let pos = Vector3::new(value[0], value[1], value[2]);
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(CFrame::new(pos, orientation).into())
}
(_, unresolved) => Err(format_err!( (_, unresolved) => Err(format_err!(
"Wrong type of value for property {}.{}. Expected {:?}, got {}", "Wrong type of value for property {}.{}. Expected {:?}, got {}",
class_name, class_name,
@@ -138,6 +153,7 @@ impl AmbiguousValue {
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",
} }
} }
} }

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,
@@ -126,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, &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);
@@ -212,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

@@ -4,7 +4,7 @@ use std::{borrow::Cow, collections::HashMap};
use rbx_dom_weak::{ use rbx_dom_weak::{
types::{Ref, Variant}, types::{Ref, Variant},
WeakDom, Instance, WeakDom,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -102,22 +102,29 @@ impl InstanceSnapshot {
} }
} }
pub fn from_tree(tree: &WeakDom, id: Ref) -> Self { #[profiling::function]
let instance = tree.get_by_ref(id).expect("instance did not exist in tree"); pub fn from_tree(tree: WeakDom, id: Ref) -> Self {
let (_, mut raw_tree) = tree.into_raw();
Self::from_raw_tree(&mut raw_tree, id)
}
fn from_raw_tree(raw_tree: &mut HashMap<Ref, Instance>, id: Ref) -> Self {
let instance = raw_tree
.remove(&id)
.expect("instance did not exist in tree");
let children = instance let children = instance
.children() .children()
.iter() .iter()
.copied() .map(|&id| Self::from_raw_tree(raw_tree, id))
.map(|id| Self::from_tree(tree, id))
.collect(); .collect();
Self { Self {
snapshot_id: Some(id), snapshot_id: Some(id),
metadata: InstanceMetadata::default(), metadata: InstanceMetadata::default(),
name: Cow::Owned(instance.name.clone()), name: Cow::Owned(instance.name),
class_name: Cow::Owned(instance.class.clone()), class_name: Cow::Owned(instance.class),
properties: instance.properties.clone(), properties: instance.properties,
children, children,
} }
} }

View File

@@ -1,6 +1,9 @@
//! Defines the algorithm for applying generated patches. //! Defines the algorithm for applying generated patches.
use std::collections::HashMap; use std::{
collections::{HashMap, HashSet},
mem::take,
};
use rbx_dom_weak::types::{Ref, Variant}; use rbx_dom_weak::types::{Ref, Variant};
@@ -12,21 +15,31 @@ use super::{
/// Consumes the input `PatchSet`, applying all of its prescribed changes to the /// Consumes the input `PatchSet`, applying all of its prescribed changes to the
/// tree and returns an `AppliedPatchSet`, which can be used to keep another /// tree and returns an `AppliedPatchSet`, which can be used to keep another
/// tree in sync with Rojo's. /// tree in sync with Rojo's.
#[profiling::function]
pub fn apply_patch_set(tree: &mut RojoTree, patch_set: PatchSet) -> AppliedPatchSet { pub fn apply_patch_set(tree: &mut RojoTree, patch_set: PatchSet) -> AppliedPatchSet {
let mut context = PatchApplyContext::default(); let mut context = PatchApplyContext::default();
for removed_id in patch_set.removed_instances { {
apply_remove_instance(&mut context, tree, removed_id); profiling::scope!("removals");
for removed_id in patch_set.removed_instances {
apply_remove_instance(&mut context, tree, removed_id);
}
} }
for add_patch in patch_set.added_instances { {
apply_add_child(&mut context, tree, add_patch.parent_id, add_patch.instance); profiling::scope!("additions");
for add_patch in patch_set.added_instances {
apply_add_child(&mut context, tree, add_patch.parent_id, add_patch.instance);
}
} }
// Updates need to be applied after additions, which reduces the complexity {
// of updates significantly. profiling::scope!("updates");
for update_patch in patch_set.updated_instances { // Updates need to be applied after additions, which reduces the complexity
apply_update_child(&mut context, tree, update_patch); // of updates significantly.
for update_patch in patch_set.updated_instances {
apply_update_child(&mut context, tree, update_patch);
}
} }
finalize_patch_application(context, tree) finalize_patch_application(context, tree)
@@ -55,20 +68,9 @@ struct PatchApplyContext {
/// eachother. /// eachother.
snapshot_id_to_instance_id: HashMap<Ref, Ref>, snapshot_id_to_instance_id: HashMap<Ref, Ref>,
/// The properties of instances added by the current `PatchSet`. /// Tracks all of the instances added by this patch that have refs that need
/// /// to be rewritten.
/// Instances added to the tree can refer to eachother via Ref properties, has_refs_to_rewrite: HashSet<Ref>,
/// but we need to make sure they're correctly transformed from snapshot
/// space into tree space (via `snapshot_id_to_instance_id`).
///
/// It's not possible to do that transformation for refs that refer to added
/// instances until all the instances have actually been inserted into the
/// tree. For simplicity, we defer application of _all_ properties on added
/// instances instead of just Refs.
///
/// This doesn't affect updated instances, since they're always applied
/// after we've added all the instances from the patch.
added_instance_properties: HashMap<Ref, HashMap<String, Variant>>,
/// The current applied patch result, describing changes made to the tree. /// The current applied patch result, describing changes made to the tree.
applied_patch_set: AppliedPatchSet, applied_patch_set: AppliedPatchSet,
@@ -84,23 +86,22 @@ struct PatchApplyContext {
/// The remaining Ref properties need to be handled during patch application, /// The remaining Ref properties need to be handled during patch application,
/// where we build up a map of snapshot IDs to instance IDs as they're created, /// where we build up a map of snapshot IDs to instance IDs as they're created,
/// then apply properties all at once at the end. /// then apply properties all at once at the end.
#[profiling::function]
fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -> AppliedPatchSet { fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -> AppliedPatchSet {
for (id, properties) in context.added_instance_properties { for id in context.has_refs_to_rewrite {
// This should always succeed since instances marked as added in our // This should always succeed since instances marked as added in our
// patch should be added without fail. // patch should be added without fail.
let mut instance = tree let mut instance = tree
.get_instance_mut(id) .get_instance_mut(id)
.expect("Invalid instance ID in deferred property map"); .expect("Invalid instance ID in deferred property map");
for (key, mut property_value) in properties { for value in instance.properties_mut().values_mut() {
if let Variant::Ref(referent) = property_value { if let Variant::Ref(referent) = value {
if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(&referent) if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(&referent)
{ {
property_value = Variant::Ref(instance_referent); *value = Variant::Ref(instance_referent);
} }
} }
instance.properties_mut().insert(key, property_value);
} }
} }
@@ -116,24 +117,24 @@ fn apply_add_child(
context: &mut PatchApplyContext, context: &mut PatchApplyContext,
tree: &mut RojoTree, tree: &mut RojoTree,
parent_id: Ref, parent_id: Ref,
snapshot: InstanceSnapshot, mut snapshot: InstanceSnapshot,
) { ) {
let snapshot_id = snapshot.snapshot_id; let snapshot_id = snapshot.snapshot_id;
let properties = snapshot.properties; let children = take(&mut snapshot.children);
let children = snapshot.children;
// Property application is deferred until after all children // If an object we're adding has a non-null referent, we'll note this
// are constructed. This helps apply referents correctly. // instance down as needing to be revisited later.
let remaining_snapshot = InstanceSnapshot::new() let has_refs = snapshot.properties.values().any(|value| match value {
.name(snapshot.name) Variant::Ref(value) => value.is_some(),
.class_name(snapshot.class_name) _ => false,
.metadata(snapshot.metadata) });
.snapshot_id(snapshot.snapshot_id);
let id = tree.insert_instance(parent_id, remaining_snapshot); let id = tree.insert_instance(parent_id, snapshot);
context.applied_patch_set.added.push(id); context.applied_patch_set.added.push(id);
context.added_instance_properties.insert(id, properties); if has_refs {
context.has_refs_to_rewrite.insert(id);
}
if let Some(snapshot_id) = snapshot_id { if let Some(snapshot_id) = snapshot_id {
context.snapshot_id_to_instance_id.insert(snapshot_id, id); context.snapshot_id_to_instance_id.insert(snapshot_id, id);

View File

@@ -1,7 +1,10 @@
//! Defines the algorithm for computing a roughly-minimal patch set given an //! Defines the algorithm for computing a roughly-minimal patch set given an
//! existing instance tree and an instance snapshot. //! existing instance tree and an instance snapshot.
use std::collections::{HashMap, HashSet}; use std::{
collections::{HashMap, HashSet},
mem::take,
};
use rbx_dom_weak::types::{Ref, Variant}; use rbx_dom_weak::types::{Ref, Variant};
@@ -10,16 +13,24 @@ use super::{
InstanceSnapshot, InstanceWithMeta, RojoTree, InstanceSnapshot, InstanceWithMeta, RojoTree,
}; };
pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: Ref) -> PatchSet { #[profiling::function]
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
} }
@@ -63,7 +74,7 @@ fn rewrite_refs_in_snapshot(context: &ComputePatchContext, snapshot: &mut Instan
fn compute_patch_set_internal( fn compute_patch_set_internal(
context: &mut ComputePatchContext, context: &mut ComputePatchContext,
snapshot: &InstanceSnapshot, mut snapshot: InstanceSnapshot,
tree: &RojoTree, tree: &RojoTree,
id: Ref, id: Ref,
patch_set: &mut PatchSet, patch_set: &mut PatchSet,
@@ -76,12 +87,12 @@ fn compute_patch_set_internal(
.get_instance(id) .get_instance(id)
.expect("Instance did not exist in tree"); .expect("Instance did not exist in tree");
compute_property_patches(snapshot, &instance, patch_set); compute_property_patches(&mut snapshot, &instance, patch_set);
compute_children_patches(context, snapshot, tree, id, patch_set); compute_children_patches(context, &mut snapshot, tree, id, patch_set);
} }
fn compute_property_patches( fn compute_property_patches(
snapshot: &InstanceSnapshot, snapshot: &mut InstanceSnapshot,
instance: &InstanceWithMeta, instance: &InstanceWithMeta,
patch_set: &mut PatchSet, patch_set: &mut PatchSet,
) { ) {
@@ -91,32 +102,32 @@ fn compute_property_patches(
let changed_name = if snapshot.name == instance.name() { let changed_name = if snapshot.name == instance.name() {
None None
} else { } else {
Some(snapshot.name.clone().into_owned()) Some(take(&mut snapshot.name).into_owned())
}; };
let changed_class_name = if snapshot.class_name == instance.class_name() { let changed_class_name = if snapshot.class_name == instance.class_name() {
None None
} else { } else {
Some(snapshot.class_name.clone().into_owned()) Some(take(&mut snapshot.class_name).into_owned())
}; };
let changed_metadata = if &snapshot.metadata == instance.metadata() { let changed_metadata = if &snapshot.metadata == instance.metadata() {
None None
} else { } else {
Some(snapshot.metadata.clone()) Some(take(&mut snapshot.metadata))
}; };
for (name, snapshot_value) in &snapshot.properties { for (name, snapshot_value) in take(&mut snapshot.properties) {
visited_properties.insert(name.as_str()); visited_properties.insert(name.clone());
match instance.properties().get(name) { match instance.properties().get(&name) {
Some(instance_value) => { Some(instance_value) => {
if snapshot_value != instance_value { if &snapshot_value != instance_value {
changed_properties.insert(name.clone(), Some(snapshot_value.clone())); changed_properties.insert(name, Some(snapshot_value));
} }
} }
None => { None => {
changed_properties.insert(name.clone(), Some(snapshot_value.clone())); changed_properties.insert(name, Some(snapshot_value));
} }
} }
} }
@@ -148,7 +159,7 @@ fn compute_property_patches(
fn compute_children_patches( fn compute_children_patches(
context: &mut ComputePatchContext, context: &mut ComputePatchContext,
snapshot: &InstanceSnapshot, snapshot: &mut InstanceSnapshot,
tree: &RojoTree, tree: &RojoTree,
id: Ref, id: Ref,
patch_set: &mut PatchSet, patch_set: &mut PatchSet,
@@ -161,7 +172,7 @@ fn compute_children_patches(
let mut paired_instances = vec![false; instance_children.len()]; let mut paired_instances = vec![false; instance_children.len()];
for snapshot_child in snapshot.children.iter() { for snapshot_child in take(&mut snapshot.children) {
let matching_instance = let matching_instance =
instance_children instance_children
.iter() .iter()
@@ -198,7 +209,7 @@ fn compute_children_patches(
None => { None => {
patch_set.added_instances.push(PatchAdd { patch_set.added_instances.push(PatchAdd {
parent_id: id, parent_id: id,
instance: snapshot_child.clone(), instance: snapshot_child,
}); });
} }
} }
@@ -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 {

View File

@@ -23,7 +23,7 @@ fn set_name_and_class_name() {
children: Vec::new(), children: Vec::new(),
}; };
let patch_set = compute_patch_set(&snapshot, &tree, tree.get_root_id()); let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set); let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value); assert_yaml_snapshot!(patch_value);
@@ -47,7 +47,7 @@ fn set_property() {
children: Vec::new(), children: Vec::new(),
}; };
let patch_set = compute_patch_set(&snapshot, &tree, tree.get_root_id()); let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set); let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value); assert_yaml_snapshot!(patch_value);
@@ -78,7 +78,7 @@ fn remove_property() {
children: Vec::new(), children: Vec::new(),
}; };
let patch_set = compute_patch_set(&snapshot, &tree, tree.get_root_id()); let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set); let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value); assert_yaml_snapshot!(patch_value);
@@ -107,7 +107,7 @@ fn add_child() {
}], }],
}; };
let patch_set = compute_patch_set(&snapshot, &tree, tree.get_root_id()); let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set); let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value); assert_yaml_snapshot!(patch_value);
@@ -139,7 +139,7 @@ fn remove_child() {
children: Vec::new(), children: Vec::new(),
}; };
let patch_set = compute_patch_set(&snapshot, &tree, tree.get_root_id()); let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set); let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value); assert_yaml_snapshot!(patch_value);

View File

@@ -87,8 +87,9 @@ impl RojoTree {
} }
pub fn insert_instance(&mut self, parent_ref: Ref, snapshot: InstanceSnapshot) -> Ref { pub fn insert_instance(&mut self, parent_ref: Ref, snapshot: InstanceSnapshot) -> Ref {
let builder = InstanceBuilder::new(snapshot.class_name.to_owned()) let builder = InstanceBuilder::empty()
.with_name(snapshot.name.to_owned()) .with_class(snapshot.class_name.into_owned())
.with_name(snapshot.name.into_owned())
.with_properties(snapshot.properties); .with_properties(snapshot.properties);
let referent = self.inner.insert(parent_ref, builder); let referent = self.inner.insert(parent_ref, builder);

View File

@@ -10,6 +10,40 @@ pub fn snapshot_dir(
context: &InstanceContext, context: &InstanceContext,
vfs: &Vfs, vfs: &Vfs,
path: &Path, path: &Path,
) -> anyhow::Result<Option<InstanceSnapshot>> {
let mut snapshot = match snapshot_dir_no_meta(context, vfs, path)? {
Some(snapshot) => snapshot,
None => return Ok(None),
};
if let Some(mut meta) = dir_meta(vfs, path)? {
meta.apply_all(&mut snapshot)?;
}
Ok(Some(snapshot))
}
/// Retrieves the meta file that should be applied for this directory, if it
/// exists.
pub fn dir_meta(vfs: &Vfs, path: &Path) -> anyhow::Result<Option<DirectoryMetadata>> {
let meta_path = path.join("init.meta.json");
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let metadata = DirectoryMetadata::from_slice(&meta_contents, meta_path)?;
Ok(Some(metadata))
} else {
Ok(None)
}
}
/// Snapshot a directory without applying meta files; useful for if the
/// directory's ClassName will change before metadata should be applied. For
/// example, this can happen if the directory contains an `init.client.lua`
/// file.
pub fn snapshot_dir_no_meta(
context: &InstanceContext,
vfs: &Vfs,
path: &Path,
) -> anyhow::Result<Option<InstanceSnapshot>> { ) -> anyhow::Result<Option<InstanceSnapshot>> {
let passes_filter_rules = |child: &DirEntry| { let passes_filter_rules = |child: &DirEntry| {
context context
@@ -52,7 +86,7 @@ pub fn snapshot_dir(
path.join("init.client.lua"), path.join("init.client.lua"),
]; ];
let mut snapshot = InstanceSnapshot::new() let snapshot = InstanceSnapshot::new()
.name(instance_name) .name(instance_name)
.class_name("Folder") .class_name("Folder")
.children(snapshot_children) .children(snapshot_children)
@@ -63,11 +97,6 @@ pub fn snapshot_dir(
.context(context), .context(context),
); );
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = DirectoryMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
}
Ok(Some(snapshot)) Ok(Some(snapshot))
} }

View File

@@ -6,7 +6,11 @@ use memofs::{IoResultExt, Vfs};
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
use super::{dir::snapshot_dir, meta_file::AdjacentMetadata, util::match_trailing}; use super::{
dir::{dir_meta, snapshot_dir_no_meta},
meta_file::AdjacentMetadata,
util::match_trailing,
};
/// Core routine for turning Lua files into snapshots. /// Core routine for turning Lua files into snapshots.
pub fn snapshot_lua( pub fn snapshot_lua(
@@ -66,14 +70,14 @@ pub fn snapshot_lua_init(
init_path: &Path, init_path: &Path,
) -> anyhow::Result<Option<InstanceSnapshot>> { ) -> anyhow::Result<Option<InstanceSnapshot>> {
let folder_path = init_path.parent().unwrap(); let folder_path = init_path.parent().unwrap();
let dir_snapshot = snapshot_dir(context, vfs, folder_path)?.unwrap(); let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path)?.unwrap();
if dir_snapshot.class_name != "Folder" { if dir_snapshot.class_name != "Folder" {
anyhow::bail!( anyhow::bail!(
"init.lua, init.server.lua, and init.client.lua can \ "init.lua, init.server.lua, and init.client.lua can \
only be used if the instance produced by the containing \ only be used if the instance produced by the containing \
directory would be a Folder.\n\n\ directory would be a Folder.\n\
\n\
The directory {} turned into an instance of class {}.", The directory {} turned into an instance of class {}.",
folder_path.display(), folder_path.display(),
dir_snapshot.class_name dir_snapshot.class_name
@@ -86,6 +90,10 @@ pub fn snapshot_lua_init(
init_snapshot.children = dir_snapshot.children; init_snapshot.children = dir_snapshot.children;
init_snapshot.metadata = dir_snapshot.metadata; init_snapshot.metadata = dir_snapshot.metadata;
if let Some(mut meta) = dir_meta(vfs, folder_path)? {
meta.apply_all(&mut init_snapshot)?;
}
Ok(Some(init_snapshot)) Ok(Some(init_snapshot))
} }

View File

@@ -40,6 +40,7 @@ pub use self::project::snapshot_project_node;
/// The main entrypoint to the snapshot function. This function can be pointed /// The main entrypoint to the snapshot function. This function can be pointed
/// at any path and will return something if Rojo knows how to deal with it. /// at any path and will return something if Rojo knows how to deal with it.
#[profiling::function]
pub fn snapshot_from_vfs( pub fn snapshot_from_vfs(
context: &InstanceContext, context: &InstanceContext,
vfs: &Vfs, vfs: &Vfs,

View File

@@ -5,7 +5,7 @@ use memofs::Vfs;
use rbx_reflection::ClassTag; use rbx_reflection::ClassTag;
use crate::{ use crate::{
project::{Project, ProjectNode}, project::{PathNode, Project, ProjectNode},
snapshot::{ snapshot::{
InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource, PathIgnoreRule, InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource, PathIgnoreRule,
}, },
@@ -30,30 +30,31 @@ pub fn snapshot_project(
context.add_path_ignore_rules(rules); context.add_path_ignore_rules(rules);
// TODO: If this project node is a path to an instance that Rojo doesn't match snapshot_project_node(&context, path, &project.name, &project.tree, vfs, None)? {
// understand, this may panic! Some(found_snapshot) => {
let mut snapshot = let mut snapshot = found_snapshot;
snapshot_project_node(&context, path, &project.name, &project.tree, vfs, None)?.unwrap(); // Setting the instigating source to the project file path is a little
// coarse.
//
// Ideally, we'd only snapshot the project file if the project file
// actually changed. Because Rojo only has the concept of one
// relevant path -> snapshot path mapping per instance, we pick the more
// conservative approach of snapshotting the project file if any
// relevant paths changed.
snapshot.metadata.instigating_source = Some(path.to_path_buf().into());
// Setting the instigating source to the project file path is a little // Mark this snapshot (the root node of the project file) as being
// coarse. // related to the project file.
// //
// Ideally, we'd only snapshot the project file if the project file // We SHOULD NOT mark the project file as a relevant path for any
// actually changed. Because Rojo only has the concept of one // nodes that aren't roots. They'll be updated as part of the project
// relevant path -> snapshot path mapping per instance, we pick the more // file being updated.
// conservative approach of snapshotting the project file if any snapshot.metadata.relevant_paths.push(path.to_path_buf());
// relevant paths changed.
snapshot.metadata.instigating_source = Some(path.to_path_buf().into());
// Mark this snapshot (the root node of the project file) as being Ok(Some(snapshot))
// related to the project file. }
// None => Ok(None),
// We SHOULD NOT mark the project file as a relevant path for any }
// nodes that aren't roots. They'll be updated as part of the project
// file being updated.
snapshot.metadata.relevant_paths.push(path.to_path_buf());
Ok(Some(snapshot))
} }
pub fn snapshot_project_node( pub fn snapshot_project_node(
@@ -77,16 +78,18 @@ pub fn snapshot_project_node(
let mut children = Vec::new(); let mut children = Vec::new();
let mut metadata = InstanceMetadata::default(); let mut metadata = InstanceMetadata::default();
if let Some(path) = &node.path { if let Some(path_node) = &node.path {
let path = path_node.path();
// If the path specified in the project is relative, we assume it's // If the path specified in the project is relative, we assume it's
// relative to the folder that the project is in, project_folder. // relative to the folder that the project is in, project_folder.
let path = if path.is_relative() { let full_path = if path.is_relative() {
Cow::Owned(project_folder.join(path)) Cow::Owned(project_folder.join(path))
} else { } else {
Cow::Borrowed(path) Cow::Borrowed(path)
}; };
if let Some(snapshot) = snapshot_from_vfs(context, vfs, &path)? { if let Some(snapshot) = snapshot_from_vfs(context, vfs, &full_path)? {
class_name_from_path = Some(snapshot.class_name); class_name_from_path = Some(snapshot.class_name);
// Properties from the snapshot are pulled in unchanged, and // Properties from the snapshot are pulled in unchanged, and
@@ -106,11 +109,6 @@ pub fn snapshot_project_node(
// Take the snapshot's metadata as-is, which will be mutated later // Take the snapshot's metadata as-is, which will be mutated later
// on. // on.
metadata = snapshot.metadata; metadata = snapshot.metadata;
} else {
// TODO: Should this issue an error instead?
log::warn!(
"$path referred to a path that could not be turned into an instance by Rojo"
);
} }
} }
@@ -120,20 +118,21 @@ pub fn snapshot_project_node(
class_name_from_project, class_name_from_project,
class_name_from_path, class_name_from_path,
class_name_from_inference, class_name_from_inference,
&node.path,
) { ) {
// These are the easy, happy paths! // These are the easy, happy paths!
(Some(project), None, None) => project, (Some(project), None, None, _) => project,
(None, Some(path), None) => path, (None, Some(path), None, _) => path,
(None, None, Some(inference)) => inference, (None, None, Some(inference), _) => inference,
// If the user specifies a class name, but there's an inferred class // If the user specifies a class name, but there's an inferred class
// name, we prefer the name listed explicitly by the user. // name, we prefer the name listed explicitly by the user.
(Some(project), None, Some(_)) => project, (Some(project), None, Some(_), _) => project,
// If the user has a $path pointing to a folder and we're able to infer // If the user has a $path pointing to a folder and we're able to infer
// a class name, let's use the inferred name. If the path we're pointing // a class name, let's use the inferred name. If the path we're pointing
// to isn't a folder, though, that's a user error. // to isn't a folder, though, that's a user error.
(None, Some(path), Some(inference)) => { (None, Some(path), Some(inference), _) => {
if path == "Folder" { if path == "Folder" {
inference inference
} else { } else {
@@ -141,7 +140,7 @@ pub fn snapshot_project_node(
} }
} }
(Some(project), Some(path), _) => { (Some(project), Some(path), _, _) => {
if path == "Folder" { if path == "Folder" {
project project
} else { } else {
@@ -155,12 +154,28 @@ pub fn snapshot_project_node(
project, project,
path, path,
project_path.display(), project_path.display(),
node.path.as_ref().unwrap().display() node.path.as_ref().unwrap().path().display()
); );
} }
} }
(None, None, None) => { (None, None, None, Some(PathNode::Optional(_))) => {
return Ok(None);
}
(_, None, _, Some(PathNode::Required(path))) => {
anyhow::bail!(
"Rojo project referred to a file using $path that could not be turned into a Roblox Instance by Rojo.\n\
Check that the file exists and is a file type known by Rojo.\n\
\n\
Project path: {}\n\
File $path: {}",
project_path.display(),
path.display(),
);
}
(None, None, None, None) => {
bail!( bail!(
"Instance \"{}\" is missing some required information.\n\ "Instance \"{}\" is missing some required information.\n\
One of the following must be true:\n\ One of the following must be true:\n\

View File

@@ -7,6 +7,7 @@ use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
use super::util::PathExt; use super::util::PathExt;
#[profiling::function]
pub fn snapshot_rbxm( pub fn snapshot_rbxm(
context: &InstanceContext, context: &InstanceContext,
vfs: &Vfs, vfs: &Vfs,
@@ -21,7 +22,8 @@ pub fn snapshot_rbxm(
let children = root_instance.children(); let children = root_instance.children();
if children.len() == 1 { if children.len() == 1 {
let snapshot = InstanceSnapshot::from_tree(&temp_tree, children[0]) let child = children[0];
let snapshot = InstanceSnapshot::from_tree(temp_tree, child)
.name(name) .name(name)
.metadata( .metadata(
InstanceMetadata::new() InstanceMetadata::new()

View File

@@ -24,7 +24,8 @@ pub fn snapshot_rbxmx(
let children = root_instance.children(); let children = root_instance.children();
if children.len() == 1 { if children.len() == 1 {
let snapshot = InstanceSnapshot::from_tree(&temp_tree, children[0]) let child = children[0];
let snapshot = InstanceSnapshot::from_tree(temp_tree, child)
.name(name) .name(name)
.metadata( .metadata(
InstanceMetadata::new() InstanceMetadata::new()

View File

@@ -11,9 +11,9 @@ use crate::{
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate}, snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
web::{ web::{
interface::{ interface::{
ErrorResponse, Instance, InstanceMetadata as WebInstanceMetadata, InstanceUpdate, ErrorResponse, Instance, OpenResponse, ReadResponse, ServerInfoResponse,
OpenResponse, ReadResponse, ServerInfoResponse, SubscribeMessage, SubscribeResponse, SubscribeMessage, SubscribeResponse, WriteRequest, WriteResponse, PROTOCOL_VERSION,
WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION, SERVER_VERSION,
}, },
util::{json, json_ok}, util::{json, json_ok},
}, },
@@ -99,44 +99,7 @@ impl ApiService {
let api_messages = messages let api_messages = messages
.into_iter() .into_iter()
.map(|message| { .map(|patch| SubscribeMessage::from_patch_update(&tree, patch))
let removed = message.removed;
let mut added = HashMap::new();
for id in message.added {
let instance = tree.get_instance(id).unwrap();
added.insert(id, Instance::from_rojo_instance(instance));
for instance in tree.descendants(id) {
added.insert(instance.id(), Instance::from_rojo_instance(instance));
}
}
let updated = message
.updated
.into_iter()
.map(|update| {
let changed_metadata = update
.changed_metadata
.as_ref()
.map(WebInstanceMetadata::from_rojo_metadata);
InstanceUpdate {
id: update.id,
changed_name: update.changed_name,
changed_class_name: update.changed_class_name,
changed_properties: update.changed_properties,
changed_metadata,
}
})
.collect();
SubscribeMessage {
removed,
added,
updated,
}
})
.collect(); .collect();
json_ok(SubscribeResponse { json_ok(SubscribeResponse {

View File

@@ -7,12 +7,14 @@ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
}; };
use rbx_dom_weak::types::{Ref, Variant}; use rbx_dom_weak::types::{Ref, Variant, VariantType};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
session_id::SessionId, session_id::SessionId,
snapshot::{InstanceMetadata as RojoInstanceMetadata, InstanceWithMeta}, snapshot::{
AppliedPatchSet, InstanceMetadata as RojoInstanceMetadata, InstanceWithMeta, RojoTree,
},
}; };
/// Server version to report over the API, not exposed outside this crate. /// Server version to report over the API, not exposed outside this crate.
@@ -30,6 +32,53 @@ pub struct SubscribeMessage<'a> {
pub updated: Vec<InstanceUpdate>, pub updated: Vec<InstanceUpdate>,
} }
impl<'a> SubscribeMessage<'a> {
pub(crate) fn from_patch_update(tree: &'a RojoTree, patch: AppliedPatchSet) -> Self {
let removed = patch.removed;
let mut added = HashMap::new();
for id in patch.added {
let instance = tree.get_instance(id).unwrap();
added.insert(id, Instance::from_rojo_instance(instance));
for instance in tree.descendants(id) {
added.insert(instance.id(), Instance::from_rojo_instance(instance));
}
}
let updated = patch
.updated
.into_iter()
.map(|update| {
let changed_metadata = update
.changed_metadata
.as_ref()
.map(InstanceMetadata::from_rojo_metadata);
let changed_properties = update
.changed_properties
.into_iter()
.filter(|(_key, value)| property_filter(value.as_ref()))
.collect();
InstanceUpdate {
id: update.id,
changed_name: update.changed_name,
changed_class_name: update.changed_class_name,
changed_properties,
changed_metadata,
}
})
.collect();
Self {
removed,
added,
updated,
}
}
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct InstanceUpdate { pub struct InstanceUpdate {
@@ -75,14 +124,8 @@ impl<'a> Instance<'a> {
let properties = source let properties = source
.properties() .properties()
.iter() .iter()
.filter_map(|(key, value)| { .filter(|(_key, value)| property_filter(Some(value)))
// SharedString values can't be serialized via Serde .map(|(key, value)| (key.clone(), Cow::Borrowed(value)))
if matches!(value, Variant::SharedString(_)) {
return None;
}
Some((key.clone(), Cow::Borrowed(value)))
})
.collect(); .collect();
Instance { Instance {
@@ -97,6 +140,18 @@ impl<'a> Instance<'a> {
} }
} }
fn property_filter(value: Option<&Variant>) -> bool {
let ty = value.map(|value| value.ty());
// Lua can't do anything with SharedString values. They also can't be
// serialized directly by Serde!
if ty == Some(VariantType::SharedString) {
return false;
}
return true;
}
/// Response body from /api/rojo /// Response body from /api/rojo
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]

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