Compare commits

...

39 Commits

Author SHA1 Message Date
Lucien Greathouse
e482aba030 Release v7.2.1 2022-07-08 20:22:16 -04:00
Samuel P
535e4d42bb Change Notification sound to generic sound (#566)
* Change Notification sound to generic sound

The notification sound causes the game to summon an error due to no experience permissions with no way to grant permission. This is due to the new audio policy update.

* Update Notification sound
2022-07-02 19:33:24 -04:00
boatbomber
54398d4c4b Add setting to toggle sound effects (#568)
* Use soundPlayer object with setting

* Style changes
2022-07-02 05:12:58 -04:00
Lucien Greathouse
0987b44e23 Release v7.2.0 2022-06-29 20:34:06 -04:00
Lucien Greathouse
58098e96d4 Update Changelog 2022-06-29 20:15:24 -04:00
Max
f649c180cf Disambiguate camelCase and PascalCase in *.meta.json and *.model.json (#563)
* Disambiguate camelCase and PascalCase.

*.meta.json forces camelCase while *.model.json forces PascalCase. This commit reinforces camelCase as the preference for both, but allows for PascalCase in both as well.

* Made requested changes, breaking due to serde bug.

* Make work with existing Serde stuff

* Work around MSRV

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-06-29 20:14:35 -04:00
Lucien Greathouse
966478b131 Update Changelog 2022-06-29 19:07:19 -04:00
boatbomber
ca0759a011 Add notification popups (#540)
* Add notifications prototype

* Add timeout

* Improve function name

* Faster timeouts and fully clickable

* Update remove padding from old X button

* Only auto-dismiss when viewport is open

* Start auto dismiss once viewed

* Avoid redundantly displaying widget text as notifs

* Add sound effect

* Add setting for notifications

* Remove duplicate PluginSettings.StudioProvider

* Use short pop sound effect

* Fix broken audio, thanks Roblox

* Use e instead of createElement
2022-06-29 19:06:13 -04:00
Lucien Greathouse
f1cdf2fe79 Update CHANGELOG 2022-06-29 19:05:04 -04:00
Watermelon
04fa5e2719 Added address reference to CLI output (#556)
* Added address reference to CLI output

* Stored loopback check address as a variable

* Changed other loopback references to the new variable

* Fixed mistake on address_string variable

* Merge write calls

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-06-29 19:04:28 -04:00
Lucien Greathouse
eccb95690c Live sync Attributes (#553)
* Add test project for tags

* Update rbx_dom_lua and add attributes project

* Add Attributes shorthand; not working

* Update dependencies

* Update rbx_reflection_database

* Update rbx_types and commit attributes snapshot
2022-06-29 18:53:34 -04:00
Samuel P
acf7456371 Accept .luau files (#552)
* accept .luau files

* Accept .luau in snapshot creation

* Update versioning and snapshots.

* fix versioning

* Run rustfmt

* Reduce repetition in extension detection

* Tidy build script change

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-06-29 18:53:10 -04:00
Lucien Greathouse
8ea41480b7 Bump MSRV (#564)
* Bump MSRV

* Fix updated snapshot
2022-06-29 18:41:37 -04:00
Lucien Greathouse
0d1bc0d7fe Update dependencies 2022-06-29 18:00:50 -04:00
JohnnyMorganz
f9b7774286 Change linux release runner to use ubuntu-18.04 (#561)
ubuntu-latest uses Ubuntu 20.04, this causes issues with glibc as older versions of ubuntu/other distros use an older version.

This is fixed by building the release binary on `ubuntu-18.04`, which uses a version of glibc more widely available.

Ref: https://github.com/JohnnyMorganz/StyLua/pull/444
https://github.com/JohnnyMorganz/StyLua/pull/445
2022-06-26 15:22:16 -04:00
Michael Schmatz
2e672badf2 Update rbx_binary to 0.6.5 (#558) 2022-06-22 04:46:34 -04:00
Lucien Greathouse
cd5d6fd15c Add test project for tags 2022-06-12 05:05:17 -04:00
Micah
cf76982cfa Update selene (#550)
* Update selene

* Update foreman.toml

Co-authored-by: Sasial <44125644+sasial-dev@users.noreply.github.com>

Co-authored-by: Sasial <44125644+sasial-dev@users.noreply.github.com>
Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-06-12 03:41:49 -04:00
Micah
2624ea7d2a Remove .luacheckrc (#551) 2022-06-12 02:57:40 -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
92 changed files with 4994 additions and 2385 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.55.0"] rust_version: [stable, 1.57.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-18.04
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

3
.gitignore vendored
View File

@@ -19,6 +19,3 @@
# Snapshot files from the 'insta' Rust crate # Snapshot files from the 'insta' Rust crate
**/*.snap.new **/*.snap.new
# Selene generates a roblox.toml file that should not be checked in.
/roblox.toml

View File

@@ -1,58 +0,0 @@
stds.roblox = {
read_globals = {
game = {
other_fields = true,
},
-- Roblox globals
"script",
-- Extra functions
"tick", "warn", "spawn",
"wait", "settings", "typeof",
-- Types
"Vector2", "Vector3",
"Vector2int16", "Vector3int16",
"Color3",
"UDim", "UDim2",
"Rect",
"CFrame",
"Enum",
"Instance",
"DockWidgetPluginGuiInfo",
}
}
stds.plugin = {
read_globals = {
"plugin",
}
}
stds.testez = {
read_globals = {
"describe",
"it", "itFOCUS", "itSKIP", "itFIXME",
"FOCUS", "SKIP", "HACK_NO_XPCALL",
"expect",
}
}
ignore = {
"212", -- unused arguments
"421", -- shadowing local variable
"422", -- shadowing argument
"431", -- shadowing upvalue
"432", -- shadowing upvalue argument
}
std = "lua51+roblox"
files["**/*.server.lua"] = {
std = "+plugin",
}
files["**/*.spec.lua"] = {
std = "+testez",
}

View File

@@ -2,6 +2,45 @@
## Unreleased Changes ## Unreleased Changes
## [7.2.1] - July 8, 2022
* Fixed notification sound by changing it to a generic sound. ([#566])
* Added setting to turn off sound effects. ([#568])
[#566]: https://github.com/rojo-rbx/rojo/pull/566
[#568]: https://github.com/rojo-rbx/rojo/pull/568
[7.2.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.2.0
## [7.2.0] - June 29, 2022
* Added support for `.luau` files. ([#552])
* Added support for live syncing Attributes and Tags. ([#553])
* Added notification popups in the Roblox Studio plugin. ([#540])
* Fixed `init.meta.json` when used with `init.lua` and related files. ([#549])
* Fixed incorrect output when serving from a non-default address or port ([#556])
* Fixed Linux binaries not running on systems with older glibc. ([#561])
* Added `camelCase` casing for JSON models, deprecating `PascalCase` names. ([#563])
* Switched from structopt to clap for command line argument parsing.
* Significantly improved performance of building and serving. ([#548])
* Increased minimum supported Rust version to 1.57.0. ([#564])
[#540]: https://github.com/rojo-rbx/rojo/pull/540
[#548]: https://github.com/rojo-rbx/rojo/pull/548
[#549]: https://github.com/rojo-rbx/rojo/pull/549
[#552]: https://github.com/rojo-rbx/rojo/pull/552
[#553]: https://github.com/rojo-rbx/rojo/pull/553
[#556]: https://github.com/rojo-rbx/rojo/pull/556
[#561]: https://github.com/rojo-rbx/rojo/pull/561
[#563]: https://github.com/rojo-rbx/rojo/pull/563
[#564]: https://github.com/rojo-rbx/rojo/pull/564
[7.2.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.2.0
## [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 ## [7.1.0] - May 22, 2022
* Added support for specifying an address to be used by default in project files. ([#507]) * 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 optional paths in project files. ([#472])

View File

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

1503
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
[package] [package]
name = "rojo" name = "rojo"
version = "7.1.0" version = "7.2.1"
rust-version = "1.57.0"
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"
@@ -8,7 +9,7 @@ homepage = "https://rojo.space"
documentation = "https://rojo.space/docs" documentation = "https://rojo.space/docs"
repository = "https://github.com/rojo-rbx/rojo" repository = "https://github.com/rojo-rbx/rojo"
readme = "README.md" readme = "README.md"
edition = "2018" edition = "2021"
build = "build.rs" build = "build.rs"
exclude = [ exclude = [
@@ -27,11 +28,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 +42,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" }
@@ -51,8 +51,8 @@ memofs = { version = "0.2.0", path = "memofs" }
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" } # rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
# rbx_xml = { path = "../rbx-dom/rbx_xml" } # rbx_xml = { path = "../rbx-dom/rbx_xml" }
rbx_binary = "0.6.4" rbx_binary = "0.6.5"
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 +69,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 +99,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"

View File

@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
Pull requests are welcome! Pull requests are welcome!
Rojo supports Rust 1.46.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has. Rojo supports Rust 1.57.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
## License ## License
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details. Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.

BIN
assets/NotificationPop.mp3 Normal file

Binary file not shown.

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

@@ -21,7 +21,7 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
// We can skip any TestEZ test files since they aren't necessary for // We can skip any TestEZ test files since they aren't necessary for
// the plugin to run. // the plugin to run.
if file_name.ends_with(".spec.lua") { if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") {
continue; continue;
} }

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.18.2" }

View File

@@ -23,8 +23,45 @@ end
local ALL_AXES = {"X", "Y", "Z"} local ALL_AXES = {"X", "Y", "Z"}
local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"} local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"}
local EncodedValue = {}
local types local types
types = { types = {
Attributes = {
fromPod = function(pod)
local output = {}
for key, value in pairs(pod) do
local ok, result = EncodedValue.decode(value)
if ok then
output[key] = result
else
local warning = ("Could not decode attribute value of type %q: %s"):format(typeof(value), tostring(result))
warn(warning)
end
end
return output
end,
toPod = function(roblox)
local output = {}
for key, value in pairs(roblox) do
local ok, result = EncodedValue.encodeNaive(value)
if ok then
output[key] = result
else
local warning = ("Could not encode attribute value of type %q: %s"):format(typeof(value), tostring(result))
warn(warning)
end
end
return output
end,
},
Axes = { Axes = {
fromPod = function(pod) fromPod = function(pod)
local axes = {} local axes = {}
@@ -433,8 +470,6 @@ types = {
}, },
} }
local EncodedValue = {}
function EncodedValue.decode(encodedValue) function EncodedValue.decode(encodedValue)
local ty, value = next(encodedValue) local ty, value = next(encodedValue)
@@ -459,4 +494,19 @@ function EncodedValue.encode(rbxValue, propertyType)
} }
end end
local propertyTypeRenames = {
number = "Float64",
boolean = "Bool",
string = "String",
}
function EncodedValue.encodeNaive(rbxValue)
local propertyType = typeof(rbxValue)
if propertyTypeRenames[propertyType] ~= nil then
propertyType = propertyTypeRenames[propertyType]
end
return EncodedValue.encode(rbxValue, propertyType)
end
return EncodedValue return EncodedValue

View File

@@ -1,4 +1,73 @@
{ {
"Attributes": {
"value": {
"Attributes": {
"TestBool": {
"Bool": true
},
"TestBrickColor": {
"BrickColor": 24
},
"TestColor3": {
"Color3": [
1.0,
0.5,
0.0
]
},
"TestNumber": {
"Float64": 1337.0
},
"TestRect": {
"Rect": [
[
1.0,
2.0
],
[
3.0,
4.0
]
]
},
"TestString": {
"String": "Test"
},
"TestUDim": {
"UDim": [
1.0,
2
]
},
"TestUDim2": {
"UDim2": [
[
1.0,
2
],
[
3.0,
4
]
]
},
"TestVector2": {
"Vector2": [
1.0,
2.0
]
},
"TestVector3": {
"Vector3": [
1.0,
2.0,
3.0
]
}
}
},
"ty": "Attributes"
},
"Axes": { "Axes": {
"value": { "value": {
"Axes": [ "Axes": [

View File

@@ -5,6 +5,26 @@ local CollectionService = game:GetService("CollectionService")
-- The reflection database refers to these as having scriptability = "Custom" -- The reflection database refers to these as having scriptability = "Custom"
return { return {
Instance = { Instance = {
Attributes = {
read = function(instance)
return true, instance:GetAttributes()
end,
write = function(instance, _, value)
local existing = instance:GetAttributes()
for key, attr in pairs(value) do
instance:SetAttribute(key, attr)
end
for key in pairs(existing) do
if value[key] == nil then
instance:SetAttribute(key, nil)
end
end
return true
end,
},
Tags = { Tags = {
read = function(instance) read = function(instance)
return true, CollectionService:GetTags(instance) return true, CollectionService:GetTags(instance)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,198 @@
local TextService = game:GetService("TextService")
local StudioService = game:GetService("StudioService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Flipper = require(Rojo.Flipper)
local bindingUtil = require(script.Parent.bindingUtil)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local baseClock = DateTime.now().UnixTimestampMillis
local e = Roact.createElement
local Notification = Roact.Component:extend("Notification")
function Notification:init()
self.motor = Flipper.SingleMotor.new(0)
self.binding = bindingUtil.fromMotor(self.motor)
self.lifetime = self.props.timeout
self.motor:onStep(function(value)
if value <= 0 then
if self.props.onClose then
self.props.onClose()
end
end
end)
end
function Notification:dismiss()
self.motor:setGoal(
Flipper.Spring.new(0, {
frequency = 5,
dampingRatio = 1,
})
)
end
function Notification:didMount()
self.motor:setGoal(
Flipper.Spring.new(1, {
frequency = 3,
dampingRatio = 1,
})
)
self.props.soundPlayer:play(Assets.Sounds.Notification)
self.timeout = task.spawn(function()
local clock = os.clock()
local seen = false
while task.wait(1/10) do
local now = os.clock()
local dt = now - clock
clock = now
if not seen then
seen = StudioService.ActiveScript == nil
end
if not seen then
-- Don't run down timer before being viewed
continue
end
self.lifetime -= dt
if self.lifetime <= 0 then
self:dismiss()
break
end
end
end)
end
function Notification:willUnmount()
task.cancel(self.timeout)
end
function Notification:render()
local time = DateTime.fromUnixTimestampMillis(self.props.timestamp)
local textBounds = TextService:GetTextSize(
self.props.text,
15,
Enum.Font.GothamSemibold,
Vector2.new(350, 700)
)
local transparency = self.binding:map(function(value)
return 1 - value
end)
local size = self.binding:map(function(value)
return UDim2.fromOffset(
(35+40+textBounds.X)*value,
math.max(14+20+textBounds.Y, 32+20)
)
end)
return Theme.with(function(theme)
return e("TextButton", {
BackgroundTransparency = 1,
Size = size,
LayoutOrder = self.props.layoutOrder,
Text = "",
ClipsDescendants = true,
[Roact.Event.Activated] = function()
self:dismiss()
end,
}, {
e(BorderedContainer, {
transparency = transparency,
size = UDim2.new(1, 0, 1, 0),
}, {
TextContainer = e("Frame", {
Size = UDim2.new(0, 35+textBounds.X, 1, -20),
Position = UDim2.new(0, 0, 0, 10),
BackgroundTransparency = 1
}, {
Logo = e("ImageLabel", {
ImageTransparency = transparency,
Image = Assets.Images.PluginButton,
BackgroundTransparency = 1,
Size = UDim2.new(0, 32, 0, 32),
Position = UDim2.new(0, 0, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
}),
Info = e("TextLabel", {
Text = self.props.text,
Font = Enum.Font.GothamSemibold,
TextSize = 15,
TextColor3 = theme.Notification.InfoColor,
TextTransparency = transparency,
TextXAlignment = Enum.TextXAlignment.Left,
TextWrapped = true,
Size = UDim2.new(0, textBounds.X, 0, textBounds.Y),
Position = UDim2.fromOffset(35, 0),
LayoutOrder = 1,
BackgroundTransparency = 1,
}),
Time = e("TextLabel", {
Text = time:FormatLocalTime("LTS", "en-us"),
Font = Enum.Font.Code,
TextSize = 12,
TextColor3 = theme.Notification.InfoColor,
TextTransparency = transparency,
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, -35, 0, 14),
Position = UDim2.new(0, 35, 1, -14),
LayoutOrder = 1,
BackgroundTransparency = 1,
}),
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 17),
PaddingRight = UDim.new(0, 15),
}),
})
})
end)
end
local Notifications = Roact.Component:extend("Notifications")
function Notifications:render()
local notifs = {}
for index, notif in ipairs(self.props.notifications) do
notifs[notif] = e(Notification, {
soundPlayer = self.props.soundPlayer,
text = notif.text,
timestamp = notif.timestamp,
timeout = notif.timeout,
layoutOrder = (notif.timestamp - baseClock),
onClose = function()
self.props.onClose(index)
end,
})
end
return Roact.createFragment(notifs)
end
return Notifications

View File

@@ -9,6 +9,8 @@ local Roact = require(Rojo.Roact)
local defaultSettings = { local defaultSettings = {
openScriptsExternally = false, openScriptsExternally = false,
twoWaySync = false, twoWaySync = false,
showNotifications = true,
playSounds = true,
} }
local Settings = {} local Settings = {}
@@ -118,4 +120,4 @@ end
return { return {
StudioProvider = StudioProvider, StudioProvider = StudioProvider,
with = with, with = with,
} }

View File

@@ -202,12 +202,28 @@ function SettingsPage:render()
layoutOrder = 1, layoutOrder = 1,
}), }),
ShowNotifications = e(Setting, {
id = "showNotifications",
name = "Show Notifications",
description = "Popup notifications in viewport",
transparency = self.props.transparency,
layoutOrder = 2,
}),
PlaySounds = e(Setting, {
id = "playSounds",
name = "Play Sounds",
description = "Toggle sound effects",
transparency = self.props.transparency,
layoutOrder = 3,
}),
TwoWaySync = e(Setting, { TwoWaySync = e(Setting, {
id = "twoWaySync", id = "twoWaySync",
name = "Two-Way Sync", name = "Two-Way Sync",
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem", description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 2, layoutOrder = 4,
}), }),
Layout = e("UIListLayout", { Layout = e("UIListLayout", {
@@ -227,4 +243,4 @@ function SettingsPage:render()
end) end)
end end
return SettingsPage return SettingsPage

View File

@@ -103,6 +103,10 @@ local lightTheme = strict("LightTheme", {
LogoColor = BRAND_COLOR, LogoColor = BRAND_COLOR,
VersionColor = hexColor(0x727272), VersionColor = hexColor(0x727272),
}, },
Notification = {
InfoColor = hexColor(0x00000),
CloseColor = BRAND_COLOR,
},
ErrorColor = hexColor(0x000000), ErrorColor = hexColor(0x000000),
ScrollBarColor = hexColor(0x000000), ScrollBarColor = hexColor(0x000000),
}) })
@@ -177,6 +181,10 @@ local darkTheme = strict("DarkTheme", {
LogoColor = BRAND_COLOR, LogoColor = BRAND_COLOR,
VersionColor = hexColor(0xD3D3D3) VersionColor = hexColor(0xD3D3D3)
}, },
Notification = {
InfoColor = hexColor(0xFFFFFF),
CloseColor = hexColor(0xFFFFFF),
},
ErrorColor = hexColor(0xFFFFFF), ErrorColor = hexColor(0xFFFFFF),
ScrollBarColor = hexColor(0xFFFFFF), ScrollBarColor = hexColor(0xFFFFFF),
}) })

View File

@@ -12,10 +12,12 @@ local Dictionary = require(Plugin.Dictionary)
local ServeSession = require(Plugin.ServeSession) local ServeSession = require(Plugin.ServeSession)
local ApiContext = require(Plugin.ApiContext) local ApiContext = require(Plugin.ApiContext)
local preloadAssets = require(Plugin.preloadAssets) local preloadAssets = require(Plugin.preloadAssets)
local soundPlayer = require(Plugin.soundPlayer)
local Theme = require(script.Theme) 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 Notifications = require(script.Notifications)
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction) 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)
@@ -44,10 +46,37 @@ function App:init()
self:setState({ self:setState({
appStatus = AppStatus.NotConnected, appStatus = AppStatus.NotConnected,
guiEnabled = false, guiEnabled = false,
notifications = {},
toolbarIcon = Assets.Images.PluginButton, toolbarIcon = Assets.Images.PluginButton,
}) })
end end
function App:addNotification(text: string, timeout: number?)
if not self.props.settings:get("showNotifications") then
return
end
local notifications = table.clone(self.state.notifications)
table.insert(notifications, {
text = text,
timestamp = DateTime.now().UnixTimestampMillis,
timeout = timeout or 3,
})
self:setState({
notifications = notifications,
})
end
function App:closeNotification(index: number)
local notifications = table.clone(self.state.notifications)
table.remove(notifications, index)
self:setState({
notifications = notifications,
})
end
function App:getHostAndPort() function App:getHostAndPort()
local host = self.host:getValue() local host = self.host:getValue()
local port = self.port:getValue() local port = self.port:getValue()
@@ -81,6 +110,7 @@ function App:startSession()
appStatus = AppStatus.Connecting, appStatus = AppStatus.Connecting,
toolbarIcon = Assets.Images.PluginButton, toolbarIcon = Assets.Images.PluginButton,
}) })
self:addNotification("Connecting to session...")
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)
self:setState({ self:setState({
@@ -89,8 +119,7 @@ function App:startSession()
address = address, address = address,
toolbarIcon = Assets.Images.PluginButtonConnected, toolbarIcon = Assets.Images.PluginButtonConnected,
}) })
self:addNotification(string.format("Connected to session '%s' at %s.", details, address), 5)
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
@@ -104,13 +133,13 @@ function App:startSession()
errorMessage = tostring(details), errorMessage = tostring(details),
toolbarIcon = Assets.Images.PluginButtonWarning, toolbarIcon = Assets.Images.PluginButtonWarning,
}) })
self:addNotification(tostring(details), 10)
else else
self:setState({ self:setState({
appStatus = AppStatus.NotConnected, appStatus = AppStatus.NotConnected,
toolbarIcon = Assets.Images.PluginButton, toolbarIcon = Assets.Images.PluginButton,
}) })
self:addNotification("Disconnected from session.")
Log.info("Disconnected session")
end end
end end
end) end)
@@ -154,150 +183,162 @@ 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 = createPageElement(AppStatus.NotConnected, {
host = self.host,
onHostChange = self.setHost,
port = self.port,
onPortChange = self.setPort,
onConnect = function()
self:startSession()
end,
onNavigateSettings = function()
self:setState({
appStatus = AppStatus.Settings,
})
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()
self:setState({
appStatus = AppStatus.NotConnected,
toolbarIcon = Assets.Images.PluginButton,
})
end,
}),
Background = Theme.with(function(theme)
return e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = theme.BackgroundColor,
ZIndex = 0,
BorderSizePixel = 0,
})
end),
}), }),
toggleAction = e(StudioPluginAction, { Background = Theme.with(function(theme)
name = "RojoConnection", return e("Frame", {
title = "Rojo: Connect/Disconnect", Size = UDim2.new(1, 0, 1, 0),
description = "Toggles the server for a Rojo sync session", BackgroundColor3 = theme.BackgroundColor,
icon = Assets.Images.PluginButton, ZIndex = 0,
bindable = true, BorderSizePixel = 0,
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),
}),
RojoNotifications = e("ScreenGui", {}, {
layout = e("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Bottom,
Padding = UDim.new(0, 5),
}), }),
notifs = e(Notifications, {
soundPlayer = self.props.soundPlayer,
notifications = self.state.notifications,
onClose = function(index)
self:closeNotification(index)
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,
})
}), }),
}), }),
}) })
@@ -308,10 +349,11 @@ return function(props)
plugin = props.plugin, plugin = props.plugin,
}, { }, {
App = PluginSettings.with(function(settings) App = PluginSettings.with(function(settings)
local settingsProps = Dictionary.merge(props, { local mergedProps = Dictionary.merge(props, {
settings = settings, settings = settings,
soundPlayer = soundPlayer.new(settings),
}) })
return e(App, settingsProps) return e(App, mergedProps)
end), end),
}) })
end end

View File

@@ -45,6 +45,9 @@ local Assets = {
[500] = "rbxassetid://2609138523" [500] = "rbxassetid://2609138523"
}, },
}, },
Sounds = {
Notification = "rbxassetid://203785492",
},
StartSession = "", StartSession = "",
SessionActive = "", SessionActive = "",
Configure = "", Configure = "",
@@ -62,4 +65,4 @@ end
guardForTypos("Assets", Assets) guardForTypos("Assets", Assets)
return Assets return Assets

View File

@@ -5,8 +5,8 @@ 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, 1, 0}, version = {7, 2, 1},
expectedServerVersionString = "7.0 or newer", expectedServerVersionString = "7.2 or newer",
protocolVersion = 4, protocolVersion = 4,
defaultHost = "localhost", defaultHost = "localhost",
defaultPort = 34872, defaultPort = 34872,

View File

@@ -18,7 +18,7 @@ local App = require(script.App)
local app = Roact.createElement(App, { local app = Roact.createElement(App, {
plugin = plugin plugin = plugin
}) })
local tree = Roact.mount(app, nil, "Rojo UI") local tree = Roact.mount(app, game:GetService("CoreGui"), "Rojo UI")
plugin.Unloading:Connect(function() plugin.Unloading:Connect(function()
Roact.unmount(tree) Roact.unmount(tree)
@@ -28,4 +28,4 @@ if Config.isDevBuild then
local TestEZ = require(script.Parent.TestEZ) local TestEZ = require(script.Parent.TestEZ)
require(script.runTests)(TestEZ) require(script.runTests)(TestEZ)
end end

View File

@@ -0,0 +1,35 @@
-- Sounds only play in Edit mode when parented to a plugin widget, for some reason
local plugin = plugin or script:FindFirstAncestorWhichIsA("Plugin")
local widget = plugin:CreateDockWidgetPluginGui("Rojo_soundPlayer", DockWidgetPluginGuiInfo.new(
Enum.InitialDockState.Float,
false, true,
10, 10,
10, 10
))
widget.Name = "Rojo_soundPlayer"
widget.Title = "Rojo Sound Player"
local SoundPlayer = {}
SoundPlayer.__index = SoundPlayer
function SoundPlayer.new(settings)
return setmetatable({
settings = settings,
}, SoundPlayer)
end
function SoundPlayer:play(soundId)
if self.settings and self.settings:get("playSounds") == false then return end
local sound = Instance.new("Sound")
sound.SoundId = soundId
sound.Parent = widget
sound.Ended:Connect(function()
sound:Destroy()
end)
sound:Play()
end
return SoundPlayer

View File

@@ -0,0 +1,24 @@
---
source: tests/tests/build.rs
assertion_line: 99
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">attributes</string>
</Properties>
<Item class="Folder" referent="1">
<Properties>
<string name="Name">Explicit</string>
<BinaryString name="AttributesSerialize">AgAAAAUAAABIZWxsbwIFAAAAV29ybGQGAAAAVmVjdG9yEQAAgD8AAABAAABAQA==</BinaryString>
</Properties>
</Item>
<Item class="Folder" referent="2">
<Properties>
<string name="Name">ImplicitAttributes</string>
<BinaryString name="AttributesSerialize">AgAAAAMAAABIZXkCBwAAAEdyYW5kbWEGAAAAVmVjdG9yEQAAgEAAAKBAAADAQA==</BinaryString>
</Properties>
</Item>
</Item>
</roblox>

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

@@ -1,14 +1,13 @@
--- ---
source: tests/tests/build.rs source: tests/tests/build.rs
assertion_line: 99
expression: contents expression: contents
--- ---
<roblox version="4"> <roblox version="4">
<Item class="Folder" referent="0"> <Item class="Folder" referent="0">
<Properties> <Properties>
<string name="Name">weldconstraint</string> <string name="Name">weldconstraint</string>
<BinaryString name="AttributesSerialize"> <BinaryString name="AttributesSerialize"></BinaryString>
</BinaryString>
<int64 name="SourceAssetId">-1</int64> <int64 name="SourceAssetId">-1</int64>
<BinaryString name="Tags"></BinaryString> <BinaryString name="Tags"></BinaryString>
</Properties> </Properties>
@@ -16,8 +15,7 @@ expression: contents
<Properties> <Properties>
<string name="Name">A</string> <string name="Name">A</string>
<bool name="Anchored">false</bool> <bool name="Anchored">false</bool>
<BinaryString name="AttributesSerialize"> <BinaryString name="AttributesSerialize"></BinaryString>
</BinaryString>
<float name="BackParamA">-0.5</float> <float name="BackParamA">-0.5</float>
<float name="BackParamB">0.5</float> <float name="BackParamB">0.5</float>
<token name="BackSurface">0</token> <token name="BackSurface">0</token>
@@ -108,8 +106,7 @@ expression: contents
<Item class="WeldConstraint" referent="2"> <Item class="WeldConstraint" referent="2">
<Properties> <Properties>
<string name="Name">WeldConstraint</string> <string name="Name">WeldConstraint</string>
<BinaryString name="AttributesSerialize"> <BinaryString name="AttributesSerialize"></BinaryString>
</BinaryString>
<CoordinateFrame name="CFrame0"> <CoordinateFrame name="CFrame0">
<X>7</X> <X>7</X>
<Y>0.000001013279</Y> <Y>0.000001013279</Y>
@@ -136,8 +133,7 @@ expression: contents
<Properties> <Properties>
<string name="Name">B</string> <string name="Name">B</string>
<bool name="Anchored">false</bool> <bool name="Anchored">false</bool>
<BinaryString name="AttributesSerialize"> <BinaryString name="AttributesSerialize"></BinaryString>
</BinaryString>
<float name="BackParamA">-0.5</float> <float name="BackParamA">-0.5</float>
<float name="BackParamB">0.5</float> <float name="BackParamB">0.5</float>
<token name="BackSurface">0</token> <token name="BackSurface">0</token>

View File

@@ -0,0 +1,36 @@
{
"name": "attributes",
"tree": {
"$className": "Folder",
"Explicit": {
"$className": "Folder",
"$properties": {
"Attributes": {
"Attributes": {
"Hello": {
"String": "World"
},
"Vector": {
"Vector3": [1, 2, 3]
}
}
}
}
},
"ImplicitAttributes": {
"$className": "Folder",
"$properties": {
"Attributes": {
"Hey": {
"String": "Grandma"
},
"Vector": {
"Vector3": [4, 5, 6]
}
}
}
}
}
}

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

@@ -291,7 +291,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
} }
}; };
let patch_set = compute_patch_set(snapshot.as_ref(), &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) => {
@@ -334,7 +334,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
} }
}; };
let patch_set = compute_patch_set(snapshot.as_ref(), &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;
@@ -11,7 +11,7 @@ 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;
@@ -23,15 +23,15 @@ pub use self::serve::ServeCommand;
pub use self::sourcemap::SourcemapCommand; pub use self::sourcemap::SourcemapCommand;
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,
} }
@@ -50,14 +50,14 @@ impl Options {
} }
} }
#[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,
} }
@@ -109,7 +109,7 @@ 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),

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>,
} }
@@ -67,15 +67,17 @@ fn show_start_message(bind_address: IpAddr, port: u16, color: ColorChoice) -> io
let writer = BufferWriter::stdout(color); let writer = BufferWriter::stdout(color);
let mut buffer = writer.buffer(); let mut buffer = writer.buffer();
let address_string = if bind_address.is_loopback() {
"localhost".to_owned()
} else {
bind_address.to_string()
};
writeln!(&mut buffer, "Rojo server listening:")?; writeln!(&mut buffer, "Rojo server listening:")?;
write!(&mut buffer, " Address: ")?; write!(&mut buffer, " Address: ")?;
buffer.set_color(&green)?; buffer.set_color(&green)?;
if bind_address.is_loopback() { writeln!(&mut buffer, "{}", address_string)?;
writeln!(&mut buffer, "localhost")?;
} else {
writeln!(&mut buffer, "{}", bind_address)?;
}
buffer.set_color(&ColorSpec::new())?; buffer.set_color(&ColorSpec::new())?;
write!(&mut buffer, " Port: ")?; write!(&mut buffer, " Port: ")?;
@@ -88,7 +90,7 @@ fn show_start_message(bind_address: IpAddr, port: u16, color: ColorChoice) -> io
write!(&mut buffer, "Visit ")?; write!(&mut buffer, "Visit ")?;
buffer.set_color(&green)?; buffer.set_color(&green)?;
write!(&mut buffer, "http://localhost:{}/", port)?; write!(&mut buffer, "http://{}:{}/", address_string, port)?;
buffer.set_color(&ColorSpec::new())?; buffer.set_color(&ColorSpec::new())?;
writeln!(&mut buffer, " in your browser for more information.")?; writeln!(&mut buffer, " in your browser for more information.")?;

View File

@@ -3,11 +3,11 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use clap::Parser;
use fs_err::File; use fs_err::File;
use memofs::Vfs; use memofs::Vfs;
use rbx_dom_weak::types::Ref; use rbx_dom_weak::types::Ref;
use serde::Serialize; use serde::Serialize;
use structopt::StructOpt;
use crate::{ use crate::{
serve_session::ServeSession, serve_session::ServeSession,
@@ -33,22 +33,22 @@ struct SourcemapNode {
} }
/// Generates a sourcemap file from the Rojo project. /// Generates a sourcemap file from the Rojo project.
#[derive(Debug, StructOpt)] #[derive(Debug, Parser)]
pub struct SourcemapCommand { pub struct SourcemapCommand {
/// Path to the project to use for the sourcemap. Defaults to the current /// Path to the project to use for the sourcemap. Defaults to the current
/// directory. /// directory.
#[structopt(default_value = "")] #[clap(default_value = "")]
pub project: PathBuf, pub project: PathBuf,
/// Where to output the sourcemap. Omit this to use stdout instead of /// Where to output the sourcemap. Omit this to use stdout instead of
/// writing to a file. /// writing to a file.
/// ///
/// Should end in .json. /// Should end in .json.
#[structopt(long, short)] #[clap(long, short)]
pub output: Option<PathBuf>, pub output: Option<PathBuf>,
/// If non-script files should be included or not. Defaults to false. /// If non-script files should be included or not. Defaults to false.
#[structopt(long)] #[clap(long)]
pub include_non_scripts: bool, pub include_non_scripts: bool,
} }
@@ -56,9 +56,6 @@ impl SourcemapCommand {
pub fn run(self) -> anyhow::Result<()> { pub fn run(self) -> anyhow::Result<()> {
let project_path = resolve_path(&self.project); let project_path = resolve_path(&self.project);
let mut project_dir = project_path.to_path_buf();
project_dir.pop();
log::trace!("Constructing in-memory filesystem"); log::trace!("Constructing in-memory filesystem");
let vfs = Vfs::new_default(); let vfs = Vfs::new_default();
@@ -71,7 +68,7 @@ impl SourcemapCommand {
filter_non_scripts filter_non_scripts
}; };
let root_node = recurse_create_node(&tree, tree.get_root_id(), &project_dir, filter); let root_node = recurse_create_node(&tree, tree.get_root_id(), session.root_dir(), filter);
if let Some(output_path) = self.output { if let Some(output_path) = self.output {
let mut file = BufWriter::new(File::create(&output_path)?); let mut file = BufWriter::new(File::create(&output_path)?);

View File

@@ -2,38 +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. /// API key obtained from create.roblox.com/credentials. Rojo will use the Open Cloud API when this is provided. Only supports uploading to a place.
#[structopt(long = "api_key")] #[clap(long = "api_key")]
pub api_key: Option<String>, pub api_key: Option<String>,
/// The Universe ID of the given place. Required when using the Open Cloud API. /// The Universe ID of the given place. Required when using the Open Cloud API.
#[structopt(long = "universe_id")] #[clap(long = "universe_id")]
pub universe_id: Option<u64>, 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,
} }
@@ -123,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
@@ -172,10 +172,10 @@ fn do_upload_open_cloud(
universe_id, asset_id universe_id, asset_id
); );
let client = reqwest::Client::new(); let client = reqwest::blocking::Client::new();
log::debug!("Uploading to Roblox..."); log::debug!("Uploading to Roblox...");
let mut response = client let response = client
.post(&url) .post(&url)
.header("x-api-key", api_key) .header("x-api-key", api_key)
.header(CONTENT_TYPE, "application/xml") .header(CONTENT_TYPE, "application/xml")

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

@@ -2,7 +2,8 @@ use std::borrow::Borrow;
use anyhow::format_err; use anyhow::format_err;
use rbx_dom_weak::types::{ use rbx_dom_weak::types::{
CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2, Vector3, Attributes, 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};
@@ -40,6 +41,7 @@ pub enum AmbiguousValue {
Array3([f64; 3]), Array3([f64; 3]),
Array4([f64; 4]), Array4([f64; 4]),
Array12([f64; 12]), Array12([f64; 12]),
Attributes(Attributes),
} }
impl AmbiguousValue { impl AmbiguousValue {
@@ -128,6 +130,8 @@ impl AmbiguousValue {
Ok(CFrame::new(pos, orientation).into()) Ok(CFrame::new(pos, orientation).into())
} }
(VariantType::Attributes, AmbiguousValue::Attributes(value)) => Ok(value.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,
@@ -154,6 +158,7 @@ impl AmbiguousValue {
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", AmbiguousValue::Array12(_) => "an array of twelve numbers",
AmbiguousValue::Attributes(_) => "an object containing attributes",
} }
} }
} }

View File

@@ -130,7 +130,7 @@ impl ServeSession {
let snapshot = snapshot_from_vfs(&instance_context, &vfs, &start_path)?; let snapshot = snapshot_from_vfs(&instance_context, &vfs, &start_path)?;
log::trace!("Computing initial patch set"); log::trace!("Computing initial patch set");
let patch_set = compute_patch_set(snapshot.as_ref(), &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);
@@ -216,6 +216,10 @@ impl ServeSession {
pub fn serve_address(&self) -> Option<IpAddr> { pub fn serve_address(&self) -> Option<IpAddr> {
self.root_project.serve_address 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,11 +13,8 @@ use super::{
InstanceSnapshot, InstanceWithMeta, RojoTree, InstanceSnapshot, InstanceWithMeta, RojoTree,
}; };
pub fn compute_patch_set( #[profiling::function]
snapshot: Option<&InstanceSnapshot>, pub fn compute_patch_set(snapshot: Option<InstanceSnapshot>, tree: &RojoTree, id: Ref) -> PatchSet {
tree: &RojoTree,
id: Ref,
) -> PatchSet {
let mut patch_set = PatchSet::new(); let mut patch_set = PatchSet::new();
if let Some(snapshot) = snapshot { if let Some(snapshot) = snapshot {
@@ -74,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,
@@ -87,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,
) { ) {
@@ -102,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));
} }
} }
} }
@@ -159,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,
@@ -172,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()
@@ -209,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,
}); });
} }
} }
@@ -257,7 +257,7 @@ mod test {
children: Vec::new(), children: Vec::new(),
}; };
let patch_set = compute_patch_set(Some(&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 {
@@ -307,7 +307,7 @@ mod test {
class_name: Cow::Borrowed("foo"), class_name: Cow::Borrowed("foo"),
}; };
let patch_set = compute_patch_set(Some(&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(Some(&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(Some(&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(Some(&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(Some(&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(Some(&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
@@ -48,11 +82,14 @@ pub fn snapshot_dir(
// middleware. Should we figure out a way for that function to add // middleware. Should we figure out a way for that function to add
// relevant paths to this middleware? // relevant paths to this middleware?
path.join("init.lua"), path.join("init.lua"),
path.join("init.luau"),
path.join("init.server.lua"), path.join("init.server.lua"),
path.join("init.server.luau"),
path.join("init.client.lua"), path.join("init.client.lua"),
path.join("init.client.luau"),
]; ];
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 +100,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

@@ -26,12 +26,25 @@ pub fn snapshot_json_model(
return Ok(None); return Ok(None);
} }
let instance: JsonModel = serde_json::from_str(contents_str) let mut instance: JsonModel = serde_json::from_str(contents_str)
.with_context(|| format!("File is not a valid JSON model: {}", path.display()))?; .with_context(|| format!("File is not a valid JSON model: {}", path.display()))?;
if let Some(top_level_name) = &instance.name {
let new_name = format!("{}.model.json", top_level_name);
log::warn!(
"Model at path {} had a top-level Name field. \
This field has been ignored since Rojo 6.0.\n\
Consider removing this field and renaming the file to {}.",
new_name,
path.display()
);
}
instance.name = Some(name.to_owned());
let mut snapshot = instance let mut snapshot = instance
.core .into_snapshot()
.into_snapshot(name.to_owned())
.with_context(|| format!("Could not load JSON model: {}", path.display()))?; .with_context(|| format!("Could not load JSON model: {}", path.display()))?;
snapshot.metadata = snapshot snapshot.metadata = snapshot
@@ -44,42 +57,37 @@ pub fn snapshot_json_model(
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "camelCase")]
struct JsonModel { struct JsonModel {
#[serde(alias = "Name")]
name: Option<String>, name: Option<String>,
#[serde(flatten)] #[serde(alias = "ClassName")]
core: JsonModelCore,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct JsonModelInstance {
name: String,
#[serde(flatten)]
core: JsonModelCore,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct JsonModelCore {
class_name: String, class_name: String,
#[serde(default = "Vec::new", skip_serializing_if = "Vec::is_empty")] #[serde(
children: Vec<JsonModelInstance>, alias = "Children",
default = "Vec::new",
skip_serializing_if = "Vec::is_empty"
)]
children: Vec<JsonModel>,
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")] #[serde(
alias = "Properties",
default = "HashMap::new",
skip_serializing_if = "HashMap::is_empty"
)]
properties: HashMap<String, UnresolvedValue>, properties: HashMap<String, UnresolvedValue>,
} }
impl JsonModelCore { impl JsonModel {
fn into_snapshot(self, name: String) -> anyhow::Result<InstanceSnapshot> { fn into_snapshot(self) -> anyhow::Result<InstanceSnapshot> {
let name = self.name.unwrap_or_else(|| self.class_name.clone());
let class_name = self.class_name; let class_name = self.class_name;
let mut children = Vec::with_capacity(self.children.len()); let mut children = Vec::with_capacity(self.children.len());
for child in self.children { for child in self.children {
children.push(child.core.into_snapshot(child.name)?); children.push(child.into_snapshot()?);
} }
let mut properties = HashMap::with_capacity(self.properties.len()); let mut properties = HashMap::with_capacity(self.properties.len());
@@ -113,7 +121,43 @@ mod test {
VfsSnapshot::file( VfsSnapshot::file(
r#" r#"
{ {
"Name": "children", "className": "IntValue",
"properties": {
"Value": 5
},
"children": [
{
"name": "The Child",
"className": "StringValue"
}
]
}
"#,
),
)
.unwrap();
let vfs = Vfs::new(imfs);
let instance_snapshot = snapshot_json_model(
&InstanceContext::default(),
&vfs,
Path::new("/foo.model.json"),
)
.unwrap()
.unwrap();
insta::assert_yaml_snapshot!(instance_snapshot);
}
#[test]
fn model_from_vfs_legacy() {
let mut imfs = InMemoryFs::new();
imfs.load_snapshot(
"/foo.model.json",
VfsSnapshot::file(
r#"
{
"ClassName": "IntValue", "ClassName": "IntValue",
"Properties": { "Properties": {
"Value": 5 "Value": 5
@@ -130,11 +174,11 @@ mod test {
) )
.unwrap(); .unwrap();
let mut vfs = Vfs::new(imfs); let vfs = Vfs::new(imfs);
let instance_snapshot = snapshot_json_model( let instance_snapshot = snapshot_json_model(
&InstanceContext::default(), &InstanceContext::default(),
&mut vfs, &vfs,
Path::new("/foo.model.json"), Path::new("/foo.model.json"),
) )
.unwrap() .unwrap()

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(
@@ -23,6 +27,12 @@ pub fn snapshot_lua(
("LocalScript", name) ("LocalScript", name)
} else if let Some(name) = match_trailing(&file_name, ".lua") { } else if let Some(name) = match_trailing(&file_name, ".lua") {
("ModuleScript", name) ("ModuleScript", name)
} else if let Some(name) = match_trailing(&file_name, ".server.luau") {
("Script", name)
} else if let Some(name) = match_trailing(&file_name, ".client.luau") {
("LocalScript", name)
} else if let Some(name) = match_trailing(&file_name, ".luau") {
("ModuleScript", name)
} else { } else {
return Ok(None); return Ok(None);
}; };
@@ -66,7 +76,7 @@ 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!(
@@ -86,6 +96,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,
@@ -56,16 +57,31 @@ pub fn snapshot_from_vfs(
return snapshot_project(context, vfs, &project_path); return snapshot_project(context, vfs, &project_path);
} }
let init_path = path.join("init.luau");
if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path);
}
let init_path = path.join("init.lua"); let init_path = path.join("init.lua");
if vfs.metadata(&init_path).with_not_found()?.is_some() { if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path); return snapshot_lua_init(context, vfs, &init_path);
} }
let init_path = path.join("init.server.luau");
if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path);
}
let init_path = path.join("init.server.lua"); let init_path = path.join("init.server.lua");
if vfs.metadata(&init_path).with_not_found()?.is_some() { if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path); return snapshot_lua_init(context, vfs, &init_path);
} }
let init_path = path.join("init.client.luau");
if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path);
}
let init_path = path.join("init.client.lua"); let init_path = path.join("init.client.lua");
if vfs.metadata(&init_path).with_not_found()?.is_some() { if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path); return snapshot_lua_init(context, vfs, &init_path);
@@ -73,7 +89,11 @@ pub fn snapshot_from_vfs(
snapshot_dir(context, vfs, path) snapshot_dir(context, vfs, path)
} else { } else {
if let Ok(name) = path.file_name_trim_end(".lua") { let script_name = path
.file_name_trim_end(".lua")
.or_else(|_| path.file_name_trim_end(".luau"));
if let Ok(name) = script_name {
match name { match name {
// init scripts are handled elsewhere and should not turn into // init scripts are handled elsewhere and should not turn into
// their own children. // their own children.

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,10 +11,14 @@ metadata:
- /foo - /foo
- /foo/init.meta.json - /foo/init.meta.json
- /foo/init.lua - /foo/init.lua
- /foo/init.luau
- /foo/init.server.lua - /foo/init.server.lua
- /foo/init.server.luau
- /foo/init.client.lua - /foo/init.client.lua
- /foo/init.client.luau
context: {} context: {}
name: foo name: foo
class_name: Folder class_name: Folder
properties: {} properties: {}
children: [] children: []

View File

@@ -11,8 +11,11 @@ metadata:
- /foo - /foo
- /foo/init.meta.json - /foo/init.meta.json
- /foo/init.lua - /foo/init.lua
- /foo/init.luau
- /foo/init.server.lua - /foo/init.server.lua
- /foo/init.server.luau
- /foo/init.client.lua - /foo/init.client.lua
- /foo/init.client.luau
context: {} context: {}
name: foo name: foo
class_name: Folder class_name: Folder
@@ -27,10 +30,14 @@ children:
- /foo/Child - /foo/Child
- /foo/Child/init.meta.json - /foo/Child/init.meta.json
- /foo/Child/init.lua - /foo/Child/init.lua
- /foo/Child/init.luau
- /foo/Child/init.server.lua - /foo/Child/init.server.lua
- /foo/Child/init.server.luau
- /foo/Child/init.client.lua - /foo/Child/init.client.lua
- /foo/Child/init.client.luau
context: {} context: {}
name: Child name: Child
class_name: Folder class_name: Folder
properties: {} properties: {}
children: [] children: []

View File

@@ -0,0 +1,29 @@
---
source: src/snapshot_middleware/json_model.rs
assertion_line: 186
expression: instance_snapshot
---
snapshot_id: ~
metadata:
ignore_unknown_instances: false
instigating_source:
Path: /foo.model.json
relevant_paths:
- /foo.model.json
context: {}
name: foo
class_name: IntValue
properties:
Value:
Int64: 5
children:
- snapshot_id: ~
metadata:
ignore_unknown_instances: false
relevant_paths: []
context: {}
name: The Child
class_name: StringValue
properties: {}
children: []

View File

@@ -244,7 +244,7 @@ impl ApiService {
} }
} }
/// If this instance is represented by a script, try to find the correct .lua /// If this instance is represented by a script, try to find the correct .lua or .luau
/// file to open to edit it. /// file to open to edit it.
fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option<PathBuf> { fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option<PathBuf> {
match instance.class_name() { match instance.class_name() {
@@ -252,16 +252,17 @@ fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option<PathBuf> {
_ => return None, _ => return None,
} }
// Pick the first listed relevant path that has an extension of .lua that // Pick the first listed relevant path that has an extension of .lua or .luau that
// exists. // exists.
instance instance
.metadata() .metadata()
.relevant_paths .relevant_paths
.iter() .iter()
.find(|path| { .find(|path| {
// We should only ever open Lua files to be safe. // We should only ever open Lua or Luau files to be safe.
match path.extension().and_then(|ext| ext.to_str()) { match path.extension().and_then(|ext| ext.to_str()) {
Some("lua") => {} Some("lua") => {}
Some("luau") => {}
_ => return false, _ => return false,
} }

View File

@@ -0,0 +1,16 @@
{
"name": "attributes",
"tree": {
"$className": "DataModel",
"Workspace": {
"Folder": {
"$className": "Folder",
"$properties": {
"Attributes": {
"Hello": { "Vector3": [1, 2, 3] }
}
}
}
}
}
}

View File

@@ -0,0 +1,14 @@
{
"name": "tags",
"tree": {
"$className": "DataModel",
"Workspace": {
"Folder": {
"$className": "Folder",
"$properties": {
"Tags": ["Hello", "World"]
}
}
}
}
}

View File

@@ -1,66 +0,0 @@
[[afterAll.args]]
type = "function"
[[afterEach.args]]
type = "function"
[[beforeAll.args]]
type = "function"
[[beforeEach.args]]
type = "function"
[[describe.args]]
type = "string"
[[describe.args]]
type = "function"
[[describeFOCUS.args]]
type = "string"
[[describeFOCUS.args]]
type = "function"
[[describeSKIP.args]]
type = "string"
[[describeSKIP.args]]
type = "function"
[[expect.args]]
type = "any"
[[FIXME.args]]
type = "string"
required = false
[FOCUS]
args = []
[[it.args]]
type = "string"
[[it.args]]
type = "function"
[[itFIXME.args]]
type = "string"
[[itFIXME.args]]
type = "function"
[[itFOCUS.args]]
type = "string"
[[itFOCUS.args]]
type = "function"
[[itSKIP.args]]
type = "string"
[[itSKIP.args]]
type = "function"
[SKIP]
args = []

53
testez.yml Normal file
View File

@@ -0,0 +1,53 @@
---
globals:
FIXME:
args:
- required: false
type: string
FOCUS:
args: []
SKIP:
args: []
afterAll:
args:
- type: function
afterEach:
args:
- type: function
beforeAll:
args:
- type: function
beforeEach:
args:
- type: function
describe:
args:
- type: string
- type: function
describeFOCUS:
args:
- type: string
- type: function
describeSKIP:
args:
- type: string
- type: function
expect:
args:
- type: any
it:
args:
- type: string
- type: function
itFIXME:
args:
- type: string
- type: function
itFOCUS:
args:
- type: string
- type: function
itSKIP:
args:
- type: string
- type: function

View File

@@ -141,14 +141,14 @@ impl TestServeSession {
pub fn get_api_rojo(&self) -> Result<ServerInfoResponse, reqwest::Error> { pub fn get_api_rojo(&self) -> Result<ServerInfoResponse, reqwest::Error> {
let url = format!("http://localhost:{}/api/rojo", self.port); let url = format!("http://localhost:{}/api/rojo", self.port);
let body = reqwest::get(&url)?.text()?; let body = reqwest::blocking::get(&url)?.text()?;
Ok(serde_json::from_str(&body).expect("Server returned malformed response")) Ok(serde_json::from_str(&body).expect("Server returned malformed response"))
} }
pub fn get_api_read(&self, id: Ref) -> Result<ReadResponse, reqwest::Error> { pub fn get_api_read(&self, id: Ref) -> Result<ReadResponse, reqwest::Error> {
let url = format!("http://localhost:{}/api/read/{}", self.port, id); let url = format!("http://localhost:{}/api/read/{}", self.port, id);
let body = reqwest::get(&url)?.text()?; let body = reqwest::blocking::get(&url)?.text()?;
Ok(serde_json::from_str(&body).expect("Server returned malformed response")) Ok(serde_json::from_str(&body).expect("Server returned malformed response"))
} }
@@ -159,7 +159,7 @@ impl TestServeSession {
) -> Result<SubscribeResponse<'static>, reqwest::Error> { ) -> Result<SubscribeResponse<'static>, reqwest::Error> {
let url = format!("http://localhost:{}/api/subscribe/{}", self.port, cursor); let url = format!("http://localhost:{}/api/subscribe/{}", self.port, cursor);
reqwest::get(&url)?.json() reqwest::blocking::get(&url)?.json()
} }
} }

View File

@@ -21,6 +21,7 @@ macro_rules! gen_build_tests {
} }
gen_build_tests! { gen_build_tests! {
attributes,
client_in_folder, client_in_folder,
client_init, client_init,
csv_bug_145, csv_bug_145,
@@ -36,11 +37,13 @@ gen_build_tests! {
init_meta_class_name, init_meta_class_name,
init_meta_properties, init_meta_properties,
init_with_children, init_with_children,
issue_546,
json_as_lua, json_as_lua,
json_model_in_folder, json_model_in_folder,
json_model_legacy_name, json_model_legacy_name,
module_in_folder, module_in_folder,
module_init, module_init,
optional,
project_composed_default, project_composed_default,
project_composed_file, project_composed_file,
project_root_name, project_root_name,
@@ -53,7 +56,6 @@ gen_build_tests! {
txt, txt,
txt_in_folder, txt_in_folder,
unresolved_values, unresolved_values,
optional,
weldconstraint, weldconstraint,
} }