Compare commits
1 Commits
v7.6.0
...
project-cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b96a236333 |
@@ -1,2 +0,0 @@
|
||||
((nil . ((eglot-luau-rojo-project-path . "plugin.project.json")
|
||||
(eglot-luau-rojo-sourcemap-enabled . 't))))
|
||||
@@ -23,7 +23,4 @@ insert_final_newline = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.lua]
|
||||
indent_style = tab
|
||||
|
||||
[*.luau]
|
||||
indent_style = tab
|
||||
@@ -1,2 +0,0 @@
|
||||
# stylua formatting
|
||||
0f8e1625d572a5fe0f7b5c08653ff92cc837d346
|
||||
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
*.lua linguist-language=Luau
|
||||
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
patreon: lpghatguy
|
||||
23
.github/workflows/changelog.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: Changelog Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [assigned, opened, synchronize, reopened, labeled, unlabeled]
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Check Actions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Changelog check
|
||||
uses: Zomzog/changelog-checker@v1.3.0
|
||||
with:
|
||||
fileName: CHANGELOG.md
|
||||
noChangelogLabel: skip changelog
|
||||
checkNotification: Simple
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
100
.github/workflows/ci.yml
vendored
@@ -12,28 +12,23 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Test
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-22.04, windows-latest, macos-latest, windows-11-arm, ubuntu-22.04-arm]
|
||||
rust_version: [stable, 1.55.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
toolchain: ${{ matrix.rust_version }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Build
|
||||
run: cargo build --locked --verbose
|
||||
@@ -41,93 +36,24 @@ jobs:
|
||||
- name: Test
|
||||
run: cargo test --locked --verbose
|
||||
|
||||
- name: Save Rust Cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
msrv:
|
||||
name: Check MSRV
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.83.0
|
||||
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Build
|
||||
run: cargo build --locked --verbose
|
||||
|
||||
- name: Save Rust Cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
lint:
|
||||
name: Rustfmt, Clippy, Stylua, & Selene
|
||||
name: Rustfmt and Clippy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Setup Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
with:
|
||||
version: 'v1.1.0'
|
||||
|
||||
- name: Stylua
|
||||
run: stylua --check plugin/src
|
||||
|
||||
- name: Selene
|
||||
run: selene plugin/src
|
||||
|
||||
- name: Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
|
||||
- name: Save Rust Cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
run: cargo clippy
|
||||
131
.github/workflows/release.yml
vendored
@@ -8,39 +8,49 @@ jobs:
|
||||
create-release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create ${{ github.ref_name }} --draft --verify-tag --title ${{ github.ref_name }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
|
||||
build-plugin:
|
||||
needs: ["create-release"]
|
||||
name: Build Roblox Studio Plugin
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
- name: Setup Foreman
|
||||
uses: Roblox/setup-foreman@v1
|
||||
with:
|
||||
version: 'v1.1.0'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Plugin
|
||||
run: rojo build plugin.project.json --output Rojo.rbxm
|
||||
run: rojo build plugin --output Rojo.rbxm
|
||||
|
||||
- name: Upload Plugin to Release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release upload ${{ github.ref_name }} Rojo.rbxm
|
||||
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@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rojo.rbxm
|
||||
path: Rojo.rbxm
|
||||
@@ -51,31 +61,25 @@ jobs:
|
||||
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-22.04
|
||||
os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
label: linux-x86_64
|
||||
|
||||
- host: linux
|
||||
os: ubuntu-22.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
label: linux-aarch64
|
||||
label: linux
|
||||
|
||||
- host: windows
|
||||
os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
label: windows-x86_64
|
||||
|
||||
- host: windows
|
||||
os: windows-11-arm
|
||||
target: aarch64-pc-windows-msvc
|
||||
label: windows-aarch64
|
||||
label: win64
|
||||
|
||||
- host: macos
|
||||
os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
label: macos-x86_64
|
||||
label: macos
|
||||
|
||||
- host: macos
|
||||
os: macos-latest
|
||||
@@ -87,64 +91,63 @@ jobs:
|
||||
env:
|
||||
BIN: rojo
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- 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: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
toolchain: stable
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Build Release
|
||||
run: cargo build --release --locked --verbose --target ${{ matrix.target }}
|
||||
|
||||
- name: Save Rust Cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Generate Artifact Name
|
||||
shell: bash
|
||||
run: cargo build --release --locked --verbose
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
echo "ARTIFACT_NAME=$BIN-${TAG_NAME#v}-${{ matrix.label }}.zip" >> "$GITHUB_ENV"
|
||||
# Build into a known directory so we can find our build artifact more
|
||||
# easily.
|
||||
CARGO_TARGET_DIR: output
|
||||
|
||||
- name: Create Archive and Upload to Release
|
||||
# On platforms that use OpenSSL, ensure it is statically linked to
|
||||
# make binaries more portable.
|
||||
OPENSSL_STATIC: 1
|
||||
|
||||
- name: Create Release Archive
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
mkdir staging
|
||||
|
||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||
cp "target/${{ matrix.target }}/release/$BIN.exe" staging/
|
||||
cp "output/release/$BIN.exe" staging/
|
||||
cd staging
|
||||
7z a ../$ARTIFACT_NAME *
|
||||
7z a ../release.zip *
|
||||
else
|
||||
cp "target/${{ matrix.target }}/release/$BIN" staging/
|
||||
cp "output/release/$BIN" staging/
|
||||
cd staging
|
||||
zip ../$ARTIFACT_NAME *
|
||||
zip ../release.zip *
|
||||
fi
|
||||
|
||||
gh release upload ${{ github.ref_name }} ../$ARTIFACT_NAME
|
||||
- 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@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: ${{ env.ARTIFACT_NAME }}
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
|
||||
path: release.zip
|
||||
9
.gitignore
vendored
@@ -10,8 +10,8 @@
|
||||
/*.rbxl
|
||||
/*.rbxlx
|
||||
|
||||
# Sourcemap for the Rojo plugin (for better intellisense)
|
||||
/sourcemap.json
|
||||
# Test places for the Roblox Studio Plugin
|
||||
/plugin/*.rbxlx
|
||||
|
||||
# Roblox Studio holds 'lock' files on places
|
||||
*.rbxl.lock
|
||||
@@ -20,6 +20,5 @@
|
||||
# Snapshot files from the 'insta' Rust crate
|
||||
**/*.snap.new
|
||||
|
||||
# Macos file system junk
|
||||
._*
|
||||
.DS_STORE
|
||||
# Selene generates a roblox.toml file that should not be checked in.
|
||||
/roblox.toml
|
||||
31
.gitmodules
vendored
@@ -1,18 +1,15 @@
|
||||
[submodule "plugin/Packages/Roact"]
|
||||
path = plugin/Packages/Roact
|
||||
url = https://github.com/roblox/roact.git
|
||||
[submodule "plugin/Packages/Flipper"]
|
||||
path = plugin/Packages/Flipper
|
||||
url = https://github.com/reselim/flipper.git
|
||||
[submodule "plugin/Packages/Promise"]
|
||||
path = plugin/Packages/Promise
|
||||
url = https://github.com/evaera/roblox-lua-promise.git
|
||||
[submodule "plugin/Packages/t"]
|
||||
path = plugin/Packages/t
|
||||
[submodule "plugin/modules/roact"]
|
||||
path = plugin/modules/roact
|
||||
url = https://github.com/Roblox/roact.git
|
||||
[submodule "plugin/modules/testez"]
|
||||
path = plugin/modules/testez
|
||||
url = https://github.com/Roblox/testez.git
|
||||
[submodule "plugin/modules/promise"]
|
||||
path = plugin/modules/promise
|
||||
url = https://github.com/LPGhatguy/roblox-lua-promise.git
|
||||
[submodule "plugin/modules/t"]
|
||||
path = plugin/modules/t
|
||||
url = https://github.com/osyrisrblx/t.git
|
||||
[submodule "plugin/Packages/TestEZ"]
|
||||
path = plugin/Packages/TestEZ
|
||||
url = https://github.com/roblox/testez.git
|
||||
[submodule "plugin/Packages/Highlighter"]
|
||||
path = plugin/Packages/Highlighter
|
||||
url = https://github.com/boatbomber/highlighter.git
|
||||
[submodule "plugin/modules/flipper"]
|
||||
path = plugin/modules/flipper
|
||||
url = https://github.com/Reselim/Flipper
|
||||
|
||||
58
.luacheckrc
Normal file
@@ -0,0 +1,58 @@
|
||||
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",
|
||||
}
|
||||
8
.vscode/extensions.json
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"JohnnyMorganz.luau-lsp",
|
||||
"JohnnyMorganz.stylua",
|
||||
"Kampfkarren.selene-vscode",
|
||||
"rust-lang.rust-analyzer"
|
||||
]
|
||||
}
|
||||
4
.vscode/settings.json
vendored
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"luau-lsp.sourcemap.rojoProjectFile": "plugin.project.json",
|
||||
"luau-lsp.sourcemap.autogenerate": true
|
||||
}
|
||||
445
CHANGELOG.md
@@ -1,446 +1,7 @@
|
||||
# Rojo Changelog
|
||||
|
||||
## 7.6.0 - October 10th, 2025
|
||||
* Added flag to `rojo init` to skip initializing a git repository ([#1122])
|
||||
* Added fallback method for when an Instance can't be synced through normal means ([#1030])
|
||||
This should make it possible to sync `MeshParts` and `Unions`!
|
||||
|
||||
The fallback involves deleting and recreating Instances. This will break
|
||||
properties that reference them that Rojo does not know about, so be weary.
|
||||
|
||||
* Add auto-reconnect and improve UX for sync reminders ([#1096])
|
||||
* Add support for syncing `yml` and `yaml` files (behaves similar to JSON and TOML) ([#1093])
|
||||
* Fixed colors of Table diff ([#1084])
|
||||
* Fixed `sourcemap` command outputting paths with OS-specific path separators ([#1085])
|
||||
* Fixed nil -> nil properties showing up as failing to sync in plugin's patch visualizer ([#1081])
|
||||
* Changed the background of the server's in-browser UI to be gray instead of white ([#1080])
|
||||
* Fixed `Auto Connect Playtest Server` no longer functioning due to Roblox change ([#1066])
|
||||
* Added an update indicator to the version header when a new version of the plugin is available. ([#1069])
|
||||
* Added `--absolute` flag to the sourcemap subcommand, which will emit absolute paths instead of relative paths. ([#1092])
|
||||
* Fixed applying `gameId` and `placeId` before initial sync was accepted ([#1104])
|
||||
|
||||
[#1122]: https://github.com/rojo-rbx/rojo/pull/1122
|
||||
[#1030]: https://github.com/rojo-rbx/rojo/pull/1030
|
||||
[#1096]: https://github.com/rojo-rbx/rojo/pull/1096
|
||||
[#1093]: https://github.com/rojo-rbx/rojo/pull/1093
|
||||
[#1084]: https://github.com/rojo-rbx/rojo/pull/1084
|
||||
[#1085]: https://github.com/rojo-rbx/rojo/pull/1085
|
||||
[#1081]: https://github.com/rojo-rbx/rojo/pull/1081
|
||||
[#1080]: https://github.com/rojo-rbx/rojo/pull/1080
|
||||
[#1066]: https://github.com/rojo-rbx/rojo/pull/1066
|
||||
[#1069]: https://github.com/rojo-rbx/rojo/pull/1069
|
||||
[#1092]: https://github.com/rojo-rbx/rojo/pull/1092
|
||||
[#1104]: https://github.com/rojo-rbx/rojo/pull/1104
|
||||
|
||||
## 7.5.1 - April 25th, 2025
|
||||
* Fixed output spam related to `Instance.Capabilities` in the plugin
|
||||
|
||||
## 7.5.0 - April 25th, 2025
|
||||
* Fixed an edge case that caused model pivots to not be built correctly in some cases ([#1027])
|
||||
* Add `blockedPlaceIds` project config field to allow blocking place ids from being live synced ([#1021])
|
||||
* Adds support for `.plugin.lua(u)` files - this applies the `Plugin` RunContext. ([#1008])
|
||||
* Added support for Roblox's `Content` type. This replaces the old `Content` type with `ContentId` to reflect Roblox's change.
|
||||
If you were previously using the fully-qualified syntax for `Content` you will need to switch it to `ContentId`.
|
||||
* Added support for `Enum` attributes
|
||||
* Significantly improved performance of `.rbxm` parsing
|
||||
* Support for a `$schema` field in all special JSON files (`.project.json`, `.model.json`, and `.meta.json`) ([#974])
|
||||
* Projects may now manually link `Ref` properties together using `Attributes`. ([#843])
|
||||
This has two parts: using `id` or `$id` in JSON files or a `Rojo_Target` attribute, an Instance
|
||||
is given an ID. Then, that ID may be used elsewhere in the project to point to an Instance
|
||||
using an attribute named `Rojo_Target_PROP_NAME`, where `PROP_NAME` is the name of a property.
|
||||
|
||||
As an example, here is a `model.json` for an ObjectValue that refers to itself:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "arbitrary string",
|
||||
"attributes": {
|
||||
"Rojo_Target_Value": "arbitrary string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is a very rough implementation and the usage will become more ergonomic
|
||||
over time.
|
||||
|
||||
* Updated Undo/Redo history to be more robust ([#915])
|
||||
* Added popout diff visualizer for table properties like Attributes and Tags ([#834])
|
||||
* Updated Theme to use Studio colors ([#838])
|
||||
* Improved patch visualizer UX ([#883])
|
||||
* Added update notifications for newer compatible versions in the Studio plugin. ([#832])
|
||||
* Added experimental setting for Auto Connect in playtests ([#840])
|
||||
* Improved settings UI ([#886])
|
||||
* `Open Scripts Externally` option can now be changed while syncing ([#911])
|
||||
* The sync reminder notification will now tell you what was last synced and when ([#987])
|
||||
* Fixed notification and tooltip text sometimes getting cut off ([#988])
|
||||
* Projects may now specify rules for syncing files as if they had a different file extension. ([#813])
|
||||
This is specified via a new field on project files, `syncRules`:
|
||||
|
||||
```json
|
||||
{
|
||||
"syncRules": [
|
||||
{
|
||||
"pattern": "*.foo",
|
||||
"use": "text",
|
||||
"exclude": "*.exclude.foo",
|
||||
},
|
||||
{
|
||||
"pattern": "*.bar.baz",
|
||||
"use": "json",
|
||||
"suffix": ".bar.baz",
|
||||
},
|
||||
],
|
||||
"name": "SyncRulesAreCool",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `pattern` field is a glob used to match the sync rule to files. If present, the `suffix` field allows you to specify parts of a file's name get cut off by Rojo to name the Instance, including the file extension. If it isn't specified, Rojo will only cut off the first part of the file extension, up to the first dot.
|
||||
|
||||
Additionally, the `exclude` field allows files to be excluded from the sync rule if they match a pattern specified by it. If it's not present, all files that match `pattern` will be modified using the sync rule.
|
||||
|
||||
The `use` field corresponds to one of the potential file type that Rojo will currently include in a project. Files that match the provided pattern will be treated as if they had the file extension for that file type.
|
||||
|
||||
| `use` value | file extension |
|
||||
|:---------------|:----------------|
|
||||
| `serverScript` | `.server.lua` |
|
||||
| `clientScript` | `.client.lua` |
|
||||
| `moduleScript` | `.lua` |
|
||||
| `json` | `.json` |
|
||||
| `toml` | `.toml` |
|
||||
| `csv` | `.csv` |
|
||||
| `text` | `.txt` |
|
||||
| `jsonModel` | `.model.json` |
|
||||
| `rbxm` | `.rbxm` |
|
||||
| `rbxmx` | `.rbxmx` |
|
||||
| `project` | `.project.json` |
|
||||
| `ignore` | None! |
|
||||
|
||||
Additionally, there are `use` values for specific script types ([#909]):
|
||||
|
||||
| `use` value | script type |
|
||||
|:-------------------------|:---------------------------------------|
|
||||
| `legacyServerScript` | `Script` with `Enum.RunContext.Legacy` |
|
||||
| `legacyClientScript` | `LocalScript` |
|
||||
| `runContextServerScript` | `Script` with `Enum.RunContext.Server` |
|
||||
| `runContextClientScript` | `Script` with `Enum.RunContext.Client` |
|
||||
| `pluginScript` | `Script` with `Enum.RunContext.Plugin` |
|
||||
|
||||
**All** sync rules are reset between project files, so they must be specified in each one when nesting them. This is to ensure that nothing can break other projects by changing how files are synced!
|
||||
|
||||
[#813]: https://github.com/rojo-rbx/rojo/pull/813
|
||||
[#832]: https://github.com/rojo-rbx/rojo/pull/832
|
||||
[#834]: https://github.com/rojo-rbx/rojo/pull/834
|
||||
[#838]: https://github.com/rojo-rbx/rojo/pull/838
|
||||
[#840]: https://github.com/rojo-rbx/rojo/pull/840
|
||||
[#843]: https://github.com/rojo-rbx/rojo/pull/843
|
||||
[#883]: https://github.com/rojo-rbx/rojo/pull/883
|
||||
[#886]: https://github.com/rojo-rbx/rojo/pull/886
|
||||
[#909]: https://github.com/rojo-rbx/rojo/pull/909
|
||||
[#911]: https://github.com/rojo-rbx/rojo/pull/911
|
||||
[#915]: https://github.com/rojo-rbx/rojo/pull/915
|
||||
[#974]: https://github.com/rojo-rbx/rojo/pull/974
|
||||
[#987]: https://github.com/rojo-rbx/rojo/pull/987
|
||||
[#988]: https://github.com/rojo-rbx/rojo/pull/988
|
||||
[#1008]: https://github.com/rojo-rbx/rojo/pull/1008
|
||||
[#1021]: https://github.com/rojo-rbx/rojo/pull/1021
|
||||
[#1027]: https://github.com/rojo-rbx/rojo/pull/1027
|
||||
|
||||
## [7.4.4] - August 22nd, 2024
|
||||
* Fixed issue with reading attributes from `Lighting` in new place files
|
||||
* `Instance.Archivable` will now default to `true` when building a project into a binary (`rbxm`/`rbxl`) file rather than `false`.
|
||||
|
||||
## [7.4.3] - August 6th, 2024
|
||||
* Fixed issue with building binary files introduced in 7.4.2
|
||||
* Fixed `value of type nil cannot be converted to number` warning spam in output. [#955]
|
||||
|
||||
[#955]: https://github.com/rojo-rbx/rojo/pull/955
|
||||
|
||||
## [7.4.2] - July 23, 2024
|
||||
* Added Never option to Confirmation ([#893])
|
||||
* Fixed removing trailing newlines ([#903])
|
||||
* Updated the internal property database, correcting an issue with `SurfaceAppearance.Color` that was reported [here][Surface_Appearance_Color_1] and [here][Surface_Appearance_Color_2] ([#948])
|
||||
|
||||
[#893]: https://github.com/rojo-rbx/rojo/pull/893
|
||||
[#903]: https://github.com/rojo-rbx/rojo/pull/903
|
||||
[#948]: https://github.com/rojo-rbx/rojo/pull/948
|
||||
[Surface_Appearance_Color_1]: https://devforum.roblox.com/t/jailbreak-custom-character-turned-shiny-black-no-texture/3075563
|
||||
[Surface_Appearance_Color_2]: https://devforum.roblox.com/t/surfaceappearance-not-displaying-correctly/3075588
|
||||
|
||||
## [7.4.1] - February 20, 2024
|
||||
* Made the `name` field optional on project files ([#870])
|
||||
|
||||
Files named `default.project.json` inherit the name of the folder they're in and all other projects
|
||||
are named as expect (e.g. `foo.project.json` becomes an Instance named `foo`)
|
||||
|
||||
There is no change in behavior if `name` is set.
|
||||
* Fixed incorrect results when building model pivots ([#865])
|
||||
* Fixed incorrect results when serving model pivots ([#868])
|
||||
* Rojo now converts any line endings to LF, preventing spurious diffs when syncing Lua files on Windows ([#854])
|
||||
* Fixed Rojo plugin failing to connect when project contains certain unreadable properties ([#848])
|
||||
* Fixed various cases where patch visualizer would not display sync failures ([#845], [#844])
|
||||
* Fixed http error handling so Rojo can be used in Github Codespaces ([#847])
|
||||
|
||||
[#848]: https://github.com/rojo-rbx/rojo/pull/848
|
||||
[#845]: https://github.com/rojo-rbx/rojo/pull/845
|
||||
[#844]: https://github.com/rojo-rbx/rojo/pull/844
|
||||
[#847]: https://github.com/rojo-rbx/rojo/pull/847
|
||||
[#854]: https://github.com/rojo-rbx/rojo/pull/854
|
||||
[#865]: https://github.com/rojo-rbx/rojo/pull/865
|
||||
[#868]: https://github.com/rojo-rbx/rojo/pull/868
|
||||
[#870]: https://github.com/rojo-rbx/rojo/pull/870
|
||||
|
||||
## [7.4.0] - January 16, 2024
|
||||
* Improved the visualization for array properties like Tags ([#829])
|
||||
* Significantly improved performance of `rojo serve`, `rojo build --watch`, and `rojo sourcemap --watch` on macOS. ([#830])
|
||||
* Changed *.lua files that init command generates to *.luau ([#831])
|
||||
* Does not remind users to sync if the sync lock is claimed already ([#833])
|
||||
|
||||
[#829]: https://github.com/rojo-rbx/rojo/pull/829
|
||||
[#830]: https://github.com/rojo-rbx/rojo/pull/830
|
||||
[#831]: https://github.com/rojo-rbx/rojo/pull/831
|
||||
[#833]: https://github.com/rojo-rbx/rojo/pull/833
|
||||
|
||||
## [7.4.0-rc3] - October 25, 2023
|
||||
* Changed `sourcemap --watch` to only generate the sourcemap when it's necessary ([#800])
|
||||
* Switched script source property getter and setter to `ScriptEditorService` methods ([#801])
|
||||
|
||||
This ensures that the script editor reflects any changes Rojo makes to a script while it is open in the script editor.
|
||||
|
||||
* Fixed issues when handling `SecurityCapabilities` values ([#803], [#807])
|
||||
* Fixed Rojo plugin erroring out when attempting to sync attributes with invalid names ([#809])
|
||||
|
||||
[#800]: https://github.com/rojo-rbx/rojo/pull/800
|
||||
[#801]: https://github.com/rojo-rbx/rojo/pull/801
|
||||
[#803]: https://github.com/rojo-rbx/rojo/pull/803
|
||||
[#807]: https://github.com/rojo-rbx/rojo/pull/807
|
||||
[#809]: https://github.com/rojo-rbx/rojo/pull/809
|
||||
|
||||
## [7.4.0-rc2] - October 3, 2023
|
||||
* Fixed bug with parsing version for plugin validation ([#797])
|
||||
|
||||
[#797]: https://github.com/rojo-rbx/rojo/pull/797
|
||||
|
||||
## [7.4.0-rc1] - October 3, 2023
|
||||
### Additions
|
||||
#### Project format
|
||||
* Added support for `.toml` files to `$path` ([#633])
|
||||
* Added support for `Font` and `CFrame` attributes ([rbx-dom#299], [rbx-dom#296])
|
||||
* Added the `emitLegacyScripts` field to the project format ([#765]). The behavior is outlined below:
|
||||
|
||||
| `emitLegacyScripts` Value | Action Taken by Rojo |
|
||||
|---------------------------|------------------------------------------------------------------------------------------------------------------|
|
||||
| false | Rojo emits Scripts with the appropriate `RunContext` for `*.client.lua` and `*.server.lua` files in the project. |
|
||||
| true (default) | Rojo emits LocalScripts and Scripts with legacy `RunContext` (same behavior as previously). |
|
||||
|
||||
|
||||
It can be used like this:
|
||||
```json
|
||||
{
|
||||
"emitLegacyScripts": false,
|
||||
"name": "MyCoolRunContextProject",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Added `Terrain` classname inference, similar to services ([#771])
|
||||
|
||||
`Terrain` may now be defined in projects without using `$className`:
|
||||
```json
|
||||
"Workspace": {
|
||||
"Terrain": {
|
||||
"$path": "path/to/terrain.rbxm"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Added support for `Terrain.MaterialColors` ([#770])
|
||||
|
||||
`Terrain.MaterialColors` is now represented in projects in a human readable format:
|
||||
```json
|
||||
"Workspace": {
|
||||
"Terrain": {
|
||||
"$path": "path/to/terrain.rbxm"
|
||||
"$properties": {
|
||||
"MaterialColors": {
|
||||
"Grass": [10, 20, 30],
|
||||
"Asphalt": [40, 50, 60],
|
||||
"LeafyGrass": [255, 155, 55]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Added better support for `Font` properties ([#731])
|
||||
|
||||
`FontFace` properties may now be defined using implicit property syntax:
|
||||
```json
|
||||
"TextBox": {
|
||||
"$className": "TextBox",
|
||||
"$properties": {
|
||||
"FontFace": {
|
||||
"family": "rbxasset://fonts/families/RobotoMono.json",
|
||||
"weight": "Thin",
|
||||
"style": "Normal"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Patch visualizer and notifications
|
||||
* Added a setting to control patch confirmation behavior ([#774])
|
||||
|
||||
This is a new setting for controlling when the Rojo plugin prompts for confirmation before syncing. It has four options:
|
||||
* Initial (default): prompts only once for a project in a given Studio session
|
||||
* Always: always prompts for confirmation
|
||||
* Large Changes: only prompts when there are more than X changed instances. The number of instances is configurable - an additional setting for the number of instances becomes available when this option is chosen
|
||||
* Unlisted PlaceId: only prompts if the place ID is not present in servePlaceIds
|
||||
|
||||
* Added the ability to select Instances in patch visualizer ([#709])
|
||||
|
||||
Double-clicking an instance in the patch visualizer sets Roblox Studio's selection to the instance.
|
||||
|
||||
* Added a sync reminder notification. ([#689])
|
||||
|
||||
Rojo detects if you have previously synced to a place, and displays a notification reminding you to sync again:
|
||||
|
||||

|
||||
|
||||
* Added rich Source diffs in patch visualizer ([#748])
|
||||
|
||||
A "View Diff" button for script sources is now present in the patch visualizer. Clicking it displays a side-by-side diff of the script changes:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
* Patch visualizer now indicates what changes failed to apply. ([#717])
|
||||
|
||||
A clickable warning label is displayed when the Rojo plugin is unable to apply changes. Clicking the label displays precise information about which changes failed:
|
||||
|
||||

|
||||
|
||||
|
||||
#### Miscellaneous
|
||||
* Added `plugin` flag to the `build` command that outputs to the local plugins folder ([#735])
|
||||
|
||||
This is a flag that builds a Rojo project into Roblox Studio's plugins directory. This allows you to build a Rojo project and load it into Studio as a plugin without having to type the full path to the plugins directory. It can be used like this: `rojo build <PATH-TO-PROJECT> --plugin <FILE-NAME>`
|
||||
|
||||
* Added new plugin template to the `init` command ([#738])
|
||||
|
||||
This is a new template geared towards plugins. It is similar to the model template, but creates a `Script` instead of a `ModuleScript` in the `src` directory. It can be used like this: `rojo init --kind plugin`
|
||||
|
||||
* Added protection against syncing non-place projects as a place. ([#691])
|
||||
* Add buttons for navigation on the Connected page ([#722])
|
||||
|
||||
### Fixes
|
||||
* Significantly improved performance of `rojo sourcemap` ([#668])
|
||||
* Fixed the diff visualizer of connected sessions. ([#674])
|
||||
* Fixed disconnected session activity. ([#675])
|
||||
* Skip confirming patches that contain only a datamodel name change. ([#688])
|
||||
* Fix Rojo breaking when users undo/redo in Studio ([#708])
|
||||
* Improve tooltip behavior ([#723])
|
||||
* Better settings controls ([#725])
|
||||
* Rework patch visualizer with many fixes and improvements ([#713], [#726], [#755])
|
||||
|
||||
[#668]: https://github.com/rojo-rbx/rojo/pull/668
|
||||
[#674]: https://github.com/rojo-rbx/rojo/pull/674
|
||||
[#675]: https://github.com/rojo-rbx/rojo/pull/675
|
||||
[#688]: https://github.com/rojo-rbx/rojo/pull/688
|
||||
[#689]: https://github.com/rojo-rbx/rojo/pull/689
|
||||
[#691]: https://github.com/rojo-rbx/rojo/pull/691
|
||||
[#709]: https://github.com/rojo-rbx/rojo/pull/709
|
||||
[#708]: https://github.com/rojo-rbx/rojo/pull/708
|
||||
[#713]: https://github.com/rojo-rbx/rojo/pull/713
|
||||
[#717]: https://github.com/rojo-rbx/rojo/pull/717
|
||||
[#722]: https://github.com/rojo-rbx/rojo/pull/722
|
||||
[#723]: https://github.com/rojo-rbx/rojo/pull/723
|
||||
[#725]: https://github.com/rojo-rbx/rojo/pull/725
|
||||
[#726]: https://github.com/rojo-rbx/rojo/pull/726
|
||||
[#633]: https://github.com/rojo-rbx/rojo/pull/633
|
||||
[#735]: https://github.com/rojo-rbx/rojo/pull/735
|
||||
[#731]: https://github.com/rojo-rbx/rojo/pull/731
|
||||
[#738]: https://github.com/rojo-rbx/rojo/pull/738
|
||||
[#748]: https://github.com/rojo-rbx/rojo/pull/748
|
||||
[#755]: https://github.com/rojo-rbx/rojo/pull/755
|
||||
[#765]: https://github.com/rojo-rbx/rojo/pull/765
|
||||
[#770]: https://github.com/rojo-rbx/rojo/pull/770
|
||||
[#771]: https://github.com/rojo-rbx/rojo/pull/771
|
||||
[#774]: https://github.com/rojo-rbx/rojo/pull/774
|
||||
[rbx-dom#299]: https://github.com/rojo-rbx/rbx-dom/pull/299
|
||||
[rbx-dom#296]: https://github.com/rojo-rbx/rbx-dom/pull/296
|
||||
|
||||
## [7.3.0] - April 22, 2023
|
||||
* Added `$attributes` to project format. ([#574])
|
||||
* Added `--watch` flag to `rojo sourcemap`. ([#602])
|
||||
* Added support for `init.csv` files. ([#594])
|
||||
* Added real-time sync status to the Studio plugin. ([#569])
|
||||
* Added support for copying error messages to the clipboard. ([#614])
|
||||
* Added sync locking for Team Create. ([#590])
|
||||
* Added support for specifying HTTP or HTTPS protocol in plugin. ([#642])
|
||||
* Added tooltips to buttons in the Studio plugin. ([#637])
|
||||
* Added visual diffs when connecting from the Studio plugin. ([#603])
|
||||
* Host and port are now saved in the Studio plugin. ([#613])
|
||||
* Improved padding on notifications in Studio plugin. ([#589])
|
||||
* Renamed `Common` to `Shared` in the default Rojo project. ([#611])
|
||||
* Reduced the minimum size of the Studio plugin widget. ([#606])
|
||||
* Fixed current directory in `rojo fmt-project`. ([#581])
|
||||
* Fixed errors after a session has already ended. ([#587])
|
||||
* Fixed an uncommon security permission error ([#619])
|
||||
|
||||
[#569]: https://github.com/rojo-rbx/rojo/pull/569
|
||||
[#574]: https://github.com/rojo-rbx/rojo/pull/574
|
||||
[#581]: https://github.com/rojo-rbx/rojo/pull/581
|
||||
[#587]: https://github.com/rojo-rbx/rojo/pull/587
|
||||
[#589]: https://github.com/rojo-rbx/rojo/pull/589
|
||||
[#590]: https://github.com/rojo-rbx/rojo/pull/590
|
||||
[#594]: https://github.com/rojo-rbx/rojo/pull/594
|
||||
[#602]: https://github.com/rojo-rbx/rojo/pull/602
|
||||
[#603]: https://github.com/rojo-rbx/rojo/pull/603
|
||||
[#606]: https://github.com/rojo-rbx/rojo/pull/606
|
||||
[#611]: https://github.com/rojo-rbx/rojo/pull/611
|
||||
[#613]: https://github.com/rojo-rbx/rojo/pull/613
|
||||
[#614]: https://github.com/rojo-rbx/rojo/pull/614
|
||||
[#619]: https://github.com/rojo-rbx/rojo/pull/619
|
||||
[#637]: https://github.com/rojo-rbx/rojo/pull/637
|
||||
[#642]: https://github.com/rojo-rbx/rojo/pull/642
|
||||
[7.3.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.3.0
|
||||
|
||||
## [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.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.2.1
|
||||
|
||||
## [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])
|
||||
## Unreleased Changes
|
||||
* 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])
|
||||
@@ -728,7 +289,7 @@ This is a general maintenance release for the Rojo 0.5.x release series.
|
||||
|
||||
## [0.5.0 Alpha 11](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.11) (May 29, 2019)
|
||||
* Added support for implicit property values in JSON model files ([#154](https://github.com/rojo-rbx/rojo/pull/154))
|
||||
* `Content` properties can now be specified in projects and model files as regular string literals.
|
||||
* `Content` propertyes can now be specified in projects and model files as regular string literals.
|
||||
* Added support for `BrickColor` properties.
|
||||
* Added support for properties added in client release 384, like `Lighting.Technology` being set to `"ShadowMap"`.
|
||||
* Improved performance when working with XML models and places
|
||||
@@ -939,4 +500,4 @@ This is a general maintenance release for the Rojo 0.5.x release series.
|
||||
* More robust syncing with a new reconciler
|
||||
|
||||
## [0.1.0](https://github.com/rojo-rbx/rojo/releases/tag/v0.1.0) (November 29, 2017)
|
||||
* Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs)
|
||||
* Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs)
|
||||
@@ -15,29 +15,12 @@ You'll want these tools to work on Rojo:
|
||||
|
||||
* Latest stable Rust compiler
|
||||
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
|
||||
* [Rokit](https://github.com/rojo-rbx/rokit)
|
||||
* [Luau Language Server](https://github.com/JohnnyMorganz/luau-lsp) (Only needed if working on the Studio plugin.)
|
||||
|
||||
When working on the Studio plugin, we recommend using this command to automatically rebuild the plugin when you save a change:
|
||||
|
||||
*(Make sure you've enabled the Studio setting to reload plugins on file change!)*
|
||||
|
||||
```bash
|
||||
bash scripts/watch-build-plugin.sh
|
||||
```
|
||||
|
||||
You can also run the plugin's unit tests with the following:
|
||||
|
||||
*(Make sure you have `run-in-roblox` installed first!)*
|
||||
|
||||
```bash
|
||||
bash scripts/unit-test-plugin.sh
|
||||
```
|
||||
* [Foreman](https://github.com/Roblox/foreman)
|
||||
|
||||
## Documentation
|
||||
Documentation impacts way more people than the individual lines of code we write.
|
||||
|
||||
If you find any problems in the documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them.
|
||||
If you find any problems in documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them.
|
||||
|
||||
## Bug Reports and Feature Requests
|
||||
Most of the tools around Rojo try to be clear when an issue is a bug. Even if they aren't, sometimes things don't work quite right.
|
||||
|
||||
1926
Cargo.lock
generated
105
Cargo.toml
@@ -1,22 +1,19 @@
|
||||
[package]
|
||||
name = "rojo"
|
||||
version = "7.6.0"
|
||||
rust-version = "1.83"
|
||||
authors = [
|
||||
"Lucien Greathouse <me@lpghatguy.com>",
|
||||
"Micah Reid <git@dekkonot.com>",
|
||||
"Ken Loeffler <kenloef@gmail.com>",
|
||||
]
|
||||
version = "7.1.1"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
description = "Enables professional-grade development tools for Roblox developers"
|
||||
license = "MPL-2.0"
|
||||
homepage = "https://rojo.space"
|
||||
documentation = "https://rojo.space/docs"
|
||||
repository = "https://github.com/rojo-rbx/rojo"
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
build = "build.rs"
|
||||
|
||||
exclude = ["/test-projects/**"]
|
||||
exclude = [
|
||||
"/test-projects/**",
|
||||
]
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
@@ -30,10 +27,6 @@ default = []
|
||||
# Enable this feature to live-reload assets from the web UI.
|
||||
dev_live_assets = []
|
||||
|
||||
# Run Rojo with this feature to open a Tracy session.
|
||||
# Currently uses protocol v63, last supported in Tracy 0.9.1.
|
||||
profile-with-tracy = ["profiling/profile-with-tracy"]
|
||||
|
||||
[workspace]
|
||||
members = ["crates/*"]
|
||||
|
||||
@@ -46,7 +39,8 @@ name = "build"
|
||||
harness = false
|
||||
|
||||
[dependencies]
|
||||
memofs = { version = "0.3.0", path = "crates/memofs" }
|
||||
rojo-project = { path = "crates/rojo-project" }
|
||||
memofs = { version = "0.2.0", path = "crates/memofs" }
|
||||
|
||||
# These dependencies can be uncommented when working on rbx-dom simultaneously
|
||||
# rbx_binary = { path = "../rbx-dom/rbx_binary" }
|
||||
@@ -55,67 +49,58 @@ memofs = { version = "0.3.0", path = "crates/memofs" }
|
||||
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
||||
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
||||
|
||||
rbx_binary = "2.0.0"
|
||||
rbx_dom_weak = "4.0.0"
|
||||
rbx_reflection = "6.0.0"
|
||||
rbx_reflection_database = "2.0.0"
|
||||
rbx_xml = "2.0.0"
|
||||
rbx_binary = "0.6.4"
|
||||
rbx_dom_weak = "2.3.0"
|
||||
rbx_reflection = "4.2.0"
|
||||
rbx_reflection_database = "0.2.2"
|
||||
rbx_xml = "0.12.3"
|
||||
|
||||
anyhow = "1.0.80"
|
||||
backtrace = "0.3.69"
|
||||
anyhow = "1.0.44"
|
||||
backtrace = "0.3.61"
|
||||
bincode = "1.3.3"
|
||||
crossbeam-channel = "0.5.12"
|
||||
csv = "1.3.0"
|
||||
env_logger = "0.9.3"
|
||||
fs-err = "2.11.0"
|
||||
futures = "0.3.30"
|
||||
globset = "0.4.14"
|
||||
crossbeam-channel = "0.5.1"
|
||||
csv = "1.1.6"
|
||||
env_logger = "0.9.0"
|
||||
fs-err = "2.6.0"
|
||||
futures = "0.3.17"
|
||||
globset = "0.4.8"
|
||||
humantime = "2.1.0"
|
||||
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
|
||||
hyper = { version = "0.14.13", features = ["server", "tcp", "http1"] }
|
||||
jod-thread = "0.1.2"
|
||||
log = "0.4.21"
|
||||
num_cpus = "1.16.0"
|
||||
opener = "0.5.2"
|
||||
rayon = "1.9.0"
|
||||
reqwest = { version = "0.11.24", default-features = false, features = [
|
||||
"blocking",
|
||||
"json",
|
||||
"rustls-tls",
|
||||
] }
|
||||
log = "0.4.14"
|
||||
maplit = "1.0.2"
|
||||
notify = "4.0.17"
|
||||
opener = "0.5.0"
|
||||
reqwest = { version = "0.11.10", features = ["blocking", "json"] }
|
||||
ritz = "0.1.0"
|
||||
roblox_install = "1.0.0"
|
||||
serde = { version = "1.0.197", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.114"
|
||||
toml = "0.5.11"
|
||||
termcolor = "1.4.1"
|
||||
thiserror = "1.0.57"
|
||||
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread"] }
|
||||
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
||||
clap = { version = "3.2.25", features = ["derive"] }
|
||||
profiling = "1.0.15"
|
||||
yaml-rust2 = "0.10.3"
|
||||
data-encoding = "2.8.0"
|
||||
serde = { version = "1.0.130", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.68"
|
||||
termcolor = "1.1.2"
|
||||
thiserror = "1.0.30"
|
||||
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
|
||||
uuid = { version = "1.0.0", features = ["v4", "serde"] }
|
||||
clap = { version = "3.1.18", features = ["derive"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.10.1"
|
||||
|
||||
[build-dependencies]
|
||||
memofs = { version = "0.3.0", path = "crates/memofs" }
|
||||
memofs = { version = "0.2.0", path = "crates/memofs" }
|
||||
|
||||
embed-resource = "1.8.0"
|
||||
anyhow = "1.0.80"
|
||||
embed-resource = "1.6.4"
|
||||
anyhow = "1.0.44"
|
||||
bincode = "1.3.3"
|
||||
fs-err = "2.11.0"
|
||||
fs-err = "2.6.0"
|
||||
maplit = "1.0.2"
|
||||
semver = "1.0.22"
|
||||
|
||||
[dev-dependencies]
|
||||
rojo-insta-ext = { path = "crates/rojo-insta-ext" }
|
||||
|
||||
criterion = "0.3.6"
|
||||
insta = { version = "1.36.1", features = ["redactions", "yaml"] }
|
||||
paste = "1.0.14"
|
||||
pretty_assertions = "1.4.0"
|
||||
serde_yaml = "0.8.26"
|
||||
tempfile = "3.10.1"
|
||||
walkdir = "2.5.0"
|
||||
criterion = "0.3.5"
|
||||
insta = { version = "1.8.0", features = ["redactions"] }
|
||||
paste = "1.0.5"
|
||||
pretty_assertions = "1.2.1"
|
||||
serde_yaml = "0.8.21"
|
||||
tempfile = "3.2.0"
|
||||
walkdir = "2.3.2"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<a href="https://rojo.space"><img src="assets/brand_images/logo-512.png" alt="Rojo" height="217" /></a>
|
||||
<a href="https://rojo.space"><img src="assets/logo-512.png" alt="Rojo" height="217" /></a>
|
||||
</div>
|
||||
|
||||
<div> </div>
|
||||
@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
|
||||
|
||||
Pull requests are welcome!
|
||||
|
||||
Rojo supports Rust 1.83 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||
Rojo supports Rust 1.46.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||
|
||||
## License
|
||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||
@@ -2,7 +2,7 @@
|
||||
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
|
||||
|
||||
## Getting Started
|
||||
To build this library, use:
|
||||
To build this library or plugin, use:
|
||||
|
||||
```bash
|
||||
rojo build -o "{project_name}.rbxmx"
|
||||
@@ -2,4 +2,4 @@ return {
|
||||
hello = function()
|
||||
print("Hello world, from {project_name}!")
|
||||
end,
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"$className": "DataModel",
|
||||
|
||||
"ReplicatedStorage": {
|
||||
"Shared": {
|
||||
"Common": {
|
||||
"$path": "src/shared"
|
||||
}
|
||||
},
|
||||
|
Before Width: | Height: | Size: 975 B After Width: | Height: | Size: 975 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 229 B After Width: | Height: | Size: 229 B |
|
Before Width: | Height: | Size: 584 B After Width: | Height: | Size: 584 B |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 183 B |
|
Before Width: | Height: | Size: 273 B |
|
Before Width: | Height: | Size: 933 B |
|
Before Width: | Height: | Size: 241 B |
|
Before Width: | Height: | Size: 574 B |
|
Before Width: | Height: | Size: 607 B |
@@ -17,10 +17,6 @@ html {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #e7e7e7
|
||||
}
|
||||
|
||||
img {
|
||||
max-width:100%;
|
||||
max-height:100%;
|
||||
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
@@ -1 +0,0 @@
|
||||
print("Hello world, from client!")
|
||||
@@ -1 +0,0 @@
|
||||
print("Hello world, from server!")
|
||||
@@ -1,3 +0,0 @@
|
||||
return function()
|
||||
print("Hello, world!")
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
# {project_name}
|
||||
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
|
||||
|
||||
## Getting Started
|
||||
To build this plugin to your local plugins folder, use:
|
||||
|
||||
```bash
|
||||
rojo build -p "{project_name}.rbxm"
|
||||
```
|
||||
|
||||
You can include the `watch` flag to re-build it on save:
|
||||
|
||||
```bash
|
||||
rojo build -p "{project_name}.rbxm" --watch
|
||||
```
|
||||
|
||||
For more help, check out [the Rojo documentation](https://rojo.space/docs).
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "{project_name}",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# Plugin model files
|
||||
/{project_name}.rbxmx
|
||||
/{project_name}.rbxm
|
||||
@@ -1 +0,0 @@
|
||||
print("Hello world, from plugin!")
|
||||
|
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 175 B |
@@ -3,7 +3,7 @@ use std::path::Path;
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
use librojo::cli::BuildCommand;
|
||||
use librojo::cli::{build, BuildCommand};
|
||||
|
||||
pub fn benchmark_small_place(c: &mut Criterion) {
|
||||
bench_build_place(c, "Small Place", "test-projects/benchmark_small_place")
|
||||
@@ -20,7 +20,7 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
|
||||
group.bench_function("build", |b| {
|
||||
b.iter_batched(
|
||||
|| place_setup(path),
|
||||
|(_dir, options)| options.run().unwrap(),
|
||||
|(_dir, options)| build(options).unwrap(),
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
@@ -31,12 +31,11 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
|
||||
fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) {
|
||||
let dir = tempdir().unwrap();
|
||||
let input = input_path.as_ref().to_path_buf();
|
||||
let output = Some(dir.path().join("output.rbxlx"));
|
||||
let output = dir.path().join("output.rbxlx");
|
||||
|
||||
let options = BuildCommand {
|
||||
project: input,
|
||||
watch: false,
|
||||
plugin: None,
|
||||
output,
|
||||
};
|
||||
|
||||
|
||||
60
build.rs
@@ -7,7 +7,6 @@ use fs_err as fs;
|
||||
use fs_err::File;
|
||||
use maplit::hashmap;
|
||||
use memofs::VfsSnapshot;
|
||||
use semver::Version;
|
||||
|
||||
fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
@@ -20,13 +19,9 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||
|
||||
let file_name = entry.file_name().to_str().unwrap().to_owned();
|
||||
|
||||
if file_name.starts_with(".git") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We can skip any TestEZ test files since they aren't necessary for
|
||||
// the plugin to run.
|
||||
if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") {
|
||||
if file_name.ends_with(".spec.lua") {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -45,39 +40,38 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||
|
||||
let root_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||
let plugin_dir = root_dir.join("plugin");
|
||||
let templates_dir = root_dir.join("assets").join("project-templates");
|
||||
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
||||
let plugin_root = PathBuf::from(root_dir).join("plugin");
|
||||
|
||||
let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?;
|
||||
let plugin_version =
|
||||
Version::parse(fs::read_to_string(plugin_dir.join("Version.txt"))?.trim())?;
|
||||
let plugin_modules = plugin_root.join("modules");
|
||||
|
||||
assert_eq!(
|
||||
our_version, plugin_version,
|
||||
"plugin version does not match Cargo version"
|
||||
);
|
||||
|
||||
let template_snapshot = snapshot_from_fs_path(&templates_dir)?;
|
||||
|
||||
let plugin_snapshot = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => snapshot_from_fs_path(&root_dir.join("plugin.project.json"))?,
|
||||
"plugin" => VfsSnapshot::dir(hashmap! {
|
||||
"fmt" => snapshot_from_fs_path(&plugin_dir.join("fmt"))?,
|
||||
"http" => snapshot_from_fs_path(&plugin_dir.join("http"))?,
|
||||
"log" => snapshot_from_fs_path(&plugin_dir.join("log"))?,
|
||||
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_dir.join("rbx_dom_lua"))?,
|
||||
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?,
|
||||
"Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?,
|
||||
"Version.txt" => snapshot_from_fs_path(&plugin_dir.join("Version.txt"))?,
|
||||
let snapshot = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
|
||||
"fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?,
|
||||
"http" => snapshot_from_fs_path(&plugin_root.join("http"))?,
|
||||
"log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
|
||||
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
|
||||
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
|
||||
"modules" => VfsSnapshot::dir(hashmap! {
|
||||
"roact" => VfsSnapshot::dir(hashmap! {
|
||||
"src" => snapshot_from_fs_path(&plugin_modules.join("roact").join("src"))?
|
||||
}),
|
||||
"promise" => VfsSnapshot::dir(hashmap! {
|
||||
"lib" => snapshot_from_fs_path(&plugin_modules.join("promise").join("lib"))?
|
||||
}),
|
||||
"t" => VfsSnapshot::dir(hashmap! {
|
||||
"lib" => snapshot_from_fs_path(&plugin_modules.join("t").join("lib"))?
|
||||
}),
|
||||
"flipper" => VfsSnapshot::dir(hashmap! {
|
||||
"src" => snapshot_from_fs_path(&plugin_modules.join("flipper").join("src"))?
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
let template_file = File::create(Path::new(&out_dir).join("templates.bincode"))?;
|
||||
let plugin_file = File::create(Path::new(&out_dir).join("plugin.bincode"))?;
|
||||
let out_path = Path::new(&out_dir).join("plugin.bincode");
|
||||
let out_file = File::create(&out_path)?;
|
||||
|
||||
bincode::serialize_into(plugin_file, &plugin_snapshot)?;
|
||||
bincode::serialize_into(template_file, &template_snapshot)?;
|
||||
bincode::serialize_into(out_file, &snapshot)?;
|
||||
|
||||
println!("cargo:rerun-if-changed=build/windows/rojo-manifest.rc");
|
||||
println!("cargo:rerun-if-changed=build/windows/rojo.manifest");
|
||||
|
||||
@@ -2,13 +2,6 @@
|
||||
|
||||
## Unreleased Changes
|
||||
|
||||
## 0.3.0 (2024-03-15)
|
||||
* Changed `StdBackend` file watching component to use minimal recursive watches. [#830]
|
||||
* Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854]
|
||||
|
||||
[#830]: https://github.com/rojo-rbx/rojo/pull/830
|
||||
[#854]: https://github.com/rojo-rbx/rojo/pull/854
|
||||
|
||||
## 0.2.0 (2021-08-23)
|
||||
* Updated to `crossbeam-channel` 0.5.1.
|
||||
|
||||
@@ -22,4 +15,4 @@
|
||||
* Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate.
|
||||
|
||||
## 0.1.0 (2020-03-10)
|
||||
* Initial release
|
||||
* Initial release
|
||||
@@ -1,12 +1,8 @@
|
||||
[package]
|
||||
name = "memofs"
|
||||
description = "Virtual filesystem with configurable backends."
|
||||
version = "0.3.0"
|
||||
authors = [
|
||||
"Lucien Greathouse <me@lpghatguy.com>",
|
||||
"Micah Reid <git@dekkonot.com>",
|
||||
"Ken Loeffler <kenloef@gmail.com>",
|
||||
]
|
||||
version = "0.2.0"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
@@ -15,7 +11,7 @@ homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
crossbeam-channel = "0.5.12"
|
||||
fs-err = "2.11.0"
|
||||
notify = "4.0.17"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
crossbeam-channel = "0.5.1"
|
||||
fs-err = "2.3.0"
|
||||
notify = "4.0.15"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@@ -50,12 +50,6 @@ impl InMemoryFs {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryFs {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InMemoryFsInner {
|
||||
entries: HashMap<PathBuf, Entry>,
|
||||
@@ -228,17 +222,23 @@ impl VfsBackend for InMemoryFs {
|
||||
}
|
||||
|
||||
fn must_be_file<T>(path: &Path) -> io::Result<T> {
|
||||
Err(io::Error::other(format!(
|
||||
"path {} was a directory, but must be a file",
|
||||
path.display()
|
||||
)))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"path {} was a directory, but must be a file",
|
||||
path.display()
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn must_be_dir<T>(path: &Path) -> io::Result<T> {
|
||||
Err(io::Error::other(format!(
|
||||
"path {} was a file, but must be a directory",
|
||||
path.display()
|
||||
)))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"path {} was a file, but must be a directory",
|
||||
path.display()
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn not_found<T>(path: &Path) -> io::Result<T> {
|
||||
|
||||
@@ -22,9 +22,9 @@ mod noop_backend;
|
||||
mod snapshot;
|
||||
mod std_backend;
|
||||
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::{io, str};
|
||||
|
||||
pub use in_memory_fs::InMemoryFs;
|
||||
pub use noop_backend::NoopBackend;
|
||||
@@ -155,24 +155,6 @@ impl VfsInner {
|
||||
Ok(Arc::new(contents))
|
||||
}
|
||||
|
||||
fn read_to_string<P: AsRef<Path>>(&mut self, path: P) -> io::Result<Arc<String>> {
|
||||
let path = path.as_ref();
|
||||
let contents = self.backend.read(path)?;
|
||||
|
||||
if self.watch_enabled {
|
||||
self.backend.watch(path)?;
|
||||
}
|
||||
|
||||
let contents_str = str::from_utf8(&contents).map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("File was not valid UTF-8: {}", path.display()),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Arc::new(contents_str.into()))
|
||||
}
|
||||
|
||||
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let contents = contents.as_ref();
|
||||
@@ -212,8 +194,11 @@ impl VfsInner {
|
||||
}
|
||||
|
||||
fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
|
||||
if let VfsEvent::Remove(path) = event {
|
||||
let _ = self.backend.unwatch(path);
|
||||
match event {
|
||||
VfsEvent::Remove(path) => {
|
||||
let _ = self.backend.unwatch(&path);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -276,33 +261,6 @@ impl Vfs {
|
||||
self.inner.lock().unwrap().read(path)
|
||||
}
|
||||
|
||||
/// Read a file from the VFS (or from the underlying backend if it isn't
|
||||
/// resident) into a string.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::read_to_string`][std::fs::read_to_string].
|
||||
///
|
||||
/// [std::fs::read_to_string]: https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html
|
||||
#[inline]
|
||||
pub fn read_to_string<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<String>> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().read_to_string(path)
|
||||
}
|
||||
|
||||
/// Read a file from the VFS (or the underlying backend if it isn't
|
||||
/// resident) into a string, and normalize its line endings to LF.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::read_to_string`][std::fs::read_to_string], but also performs
|
||||
/// line ending normalization.
|
||||
///
|
||||
/// [std::fs::read_to_string]: https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html
|
||||
#[inline]
|
||||
pub fn read_to_string_lf_normalized<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<String>> {
|
||||
let path = path.as_ref();
|
||||
let contents = self.inner.lock().unwrap().read_to_string(path)?;
|
||||
|
||||
Ok(contents.replace("\r\n", "\n").into())
|
||||
}
|
||||
|
||||
/// Write a file to the VFS and the underlying backend.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::write`][std::fs::write].
|
||||
@@ -473,23 +431,3 @@ impl VfsLock<'_> {
|
||||
self.inner.commit_event(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{InMemoryFs, Vfs, VfsSnapshot};
|
||||
|
||||
/// https://github.com/rojo-rbx/rojo/issues/899
|
||||
#[test]
|
||||
fn read_to_string_lf_normalized_keeps_trailing_newline() {
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("test", VfsSnapshot::file("bar\r\nfoo\r\n\r\n"))
|
||||
.unwrap();
|
||||
|
||||
let vfs = Vfs::new(imfs);
|
||||
|
||||
assert_eq!(
|
||||
vfs.read_to_string_lf_normalized("test").unwrap().as_str(),
|
||||
"bar\nfoo\n\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,27 +15,45 @@ impl NoopBackend {
|
||||
|
||||
impl VfsBackend for NoopBackend {
|
||||
fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
@@ -43,16 +61,16 @@ impl VfsBackend for NoopBackend {
|
||||
}
|
||||
|
||||
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn unwatch(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NoopBackend {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashSet, io};
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
@@ -13,7 +13,6 @@ use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent};
|
||||
pub struct StdBackend {
|
||||
watcher: RecommendedWatcher,
|
||||
watcher_receiver: Receiver<VfsEvent>,
|
||||
watches: HashSet<PathBuf>,
|
||||
}
|
||||
|
||||
impl StdBackend {
|
||||
@@ -49,7 +48,6 @@ impl StdBackend {
|
||||
Self {
|
||||
watcher,
|
||||
watcher_receiver: rx,
|
||||
watches: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,28 +97,14 @@ impl VfsBackend for StdBackend {
|
||||
}
|
||||
|
||||
fn watch(&mut self, path: &Path) -> io::Result<()> {
|
||||
if self.watches.contains(path)
|
||||
|| path
|
||||
.ancestors()
|
||||
.any(|ancestor| self.watches.contains(ancestor))
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
self.watches.insert(path.to_path_buf());
|
||||
self.watcher
|
||||
.watch(path, RecursiveMode::Recursive)
|
||||
.map_err(io::Error::other)
|
||||
}
|
||||
self.watcher
|
||||
.watch(path, RecursiveMode::NonRecursive)
|
||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||
}
|
||||
|
||||
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
|
||||
self.watches.remove(path);
|
||||
self.watcher.unwatch(path).map_err(io::Error::other)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StdBackend {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
self.watcher
|
||||
.unwatch(path)
|
||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0.197"
|
||||
serde_yaml = "0.8.26"
|
||||
serde = "1.0.99"
|
||||
serde_yaml = "0.8.9"
|
||||
|
||||
@@ -5,13 +5,19 @@ use serde::Serialize;
|
||||
/// Enables redacting any value that serializes as a string.
|
||||
///
|
||||
/// Used for transforming Rojo instance IDs into something deterministic.
|
||||
#[derive(Default)]
|
||||
pub struct RedactionMap {
|
||||
ids: HashMap<String, usize>,
|
||||
last_id: usize,
|
||||
}
|
||||
|
||||
impl RedactionMap {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ids: HashMap::new(),
|
||||
last_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> {
|
||||
let id = id.to_string();
|
||||
|
||||
@@ -22,12 +28,6 @@ impl RedactionMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the numeric ID that was assigned to the provided value,
|
||||
/// if one exists.
|
||||
pub fn get_id_for_value(&self, value: impl ToString) -> Option<usize> {
|
||||
self.ids.get(&value.to_string()).cloned()
|
||||
}
|
||||
|
||||
pub fn intern(&mut self, id: impl ToString) {
|
||||
let last_id = &mut self.last_id;
|
||||
|
||||
|
||||
16
crates/rojo-project/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "rojo-project"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.57"
|
||||
globset = { version = "0.4.8", features = ["serde1"] }
|
||||
log = "0.4.17"
|
||||
rbx_dom_weak = "2.3.0"
|
||||
rbx_reflection = "4.2.0"
|
||||
rbx_reflection_database = "0.2.4"
|
||||
serde = { version = "1.0.137", features = ["derive"] }
|
||||
serde_json = "1.0.81"
|
||||
4
crates/rojo-project/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# rojo-project
|
||||
Project file format crate for [Rojo].
|
||||
|
||||
[Rojo]: https://rojo.space
|
||||
@@ -1,6 +1,3 @@
|
||||
//! Wrapper around globset's Glob type that has better serialization
|
||||
//! characteristics by coupling Glob and GlobMatcher into a single type.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use globset::{Glob as InnerGlob, GlobMatcher};
|
||||
@@ -8,6 +5,8 @@ use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub use globset::Error;
|
||||
|
||||
/// Wrapper around globset's Glob type that has better serialization
|
||||
/// characteristics by coupling Glob and GlobMatcher into a single type.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Glob {
|
||||
inner: InnerGlob,
|
||||
7
crates/rojo-project/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod glob;
|
||||
mod path_serializer;
|
||||
mod project;
|
||||
mod resolution;
|
||||
|
||||
pub use project::{OptionalPathNode, PathNode, Project, ProjectNode};
|
||||
pub use resolution::{AmbiguousValue, UnresolvedValue};
|
||||
21
crates/rojo-project/src/path_serializer.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
//! Path serializer is used to serialize absolute paths in a cross-platform way,
|
||||
//! by replacing all directory separators with /.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use serde::Serializer;
|
||||
|
||||
pub fn serialize_absolute<S, T>(path: T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
T: AsRef<Path>,
|
||||
{
|
||||
let as_str = path
|
||||
.as_ref()
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.expect("Invalid Unicode in file path, cannot serialize");
|
||||
let replaced = as_str.replace("\\", "/");
|
||||
|
||||
serializer.serialize_str(&replaced)
|
||||
}
|
||||
363
crates/rojo-project/src/project.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::net::IpAddr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::glob::Glob;
|
||||
use crate::resolution::UnresolvedValue;
|
||||
|
||||
static PROJECT_FILENAME: &str = "default.project.json";
|
||||
|
||||
/// Contains all of the configuration for a Rojo-managed project.
|
||||
///
|
||||
/// Rojo project files are stored in `.project.json` files.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct Project {
|
||||
/// The name of the top-level instance described by the project.
|
||||
pub name: String,
|
||||
|
||||
/// The tree of instances described by this project. Projects always
|
||||
/// describe at least one instance.
|
||||
pub tree: ProjectNode,
|
||||
|
||||
/// If specified, sets the default port that `rojo serve` should use when
|
||||
/// using this project for live sync.
|
||||
///
|
||||
/// Can be overriden with the `--port` flag.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub serve_port: Option<u16>,
|
||||
|
||||
/// If specified, sets the default IP address that `rojo serve` should use
|
||||
/// when using this project for live sync.
|
||||
///
|
||||
/// Can be overridden with the `--address` flag.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub serve_address: Option<IpAddr>,
|
||||
|
||||
/// If specified, contains the set of place IDs that this project is
|
||||
/// compatible with when doing live sync.
|
||||
///
|
||||
/// This setting is intended to help prevent syncing a Rojo project into the
|
||||
/// wrong Roblox place.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub serve_place_ids: Option<HashSet<u64>>,
|
||||
|
||||
/// If specified, sets the current place's place ID when connecting to the
|
||||
/// Rojo server from Roblox Studio.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub place_id: Option<u64>,
|
||||
|
||||
/// If specified, sets the current place's game ID when connecting to the
|
||||
/// Rojo server from Roblox Studio.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub game_id: Option<u64>,
|
||||
|
||||
/// A list of globs, relative to the folder the project file is in, that
|
||||
/// match files that should be excluded if Rojo encounters them.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub glob_ignore_paths: Vec<Glob>,
|
||||
|
||||
/// The path to the file that this project came from. Relative paths in the
|
||||
/// project should be considered relative to the parent of this field, also
|
||||
/// given by `Project::folder_location`.
|
||||
#[serde(skip)]
|
||||
pub file_location: PathBuf,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
/// Tells whether the given path describes a Rojo project.
|
||||
pub fn is_project_file(path: &Path) -> bool {
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(|name| name.ends_with(".project.json"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Loads a project file from a slice and a path that indicates where the
|
||||
/// project should resolve paths relative to.
|
||||
pub fn load_from_slice(contents: &[u8], project_file_location: &Path) -> anyhow::Result<Self> {
|
||||
let mut project: Self = serde_json::from_slice(&contents).with_context(|| {
|
||||
format!(
|
||||
"Error parsing Rojo project at {}",
|
||||
project_file_location.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
project.file_location = project_file_location.to_path_buf();
|
||||
project.check_compatibility();
|
||||
Ok(project)
|
||||
}
|
||||
|
||||
/// Fuzzy-find a Rojo project and load it.
|
||||
pub fn load_fuzzy(fuzzy_project_location: &Path) -> anyhow::Result<Option<Self>> {
|
||||
if let Some(project_path) = Self::locate(fuzzy_project_location) {
|
||||
let project = Self::load_exact(&project_path)?;
|
||||
|
||||
Ok(Some(project))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gives the path that all project file paths should resolve relative to.
|
||||
pub fn folder_location(&self) -> &Path {
|
||||
self.file_location.parent().unwrap()
|
||||
}
|
||||
|
||||
/// Attempt to locate a project represented by the given path.
|
||||
///
|
||||
/// This will find a project if the path refers to a `.project.json` file,
|
||||
/// or is a folder that contains a `default.project.json` file.
|
||||
fn locate(path: &Path) -> Option<PathBuf> {
|
||||
let meta = fs::metadata(path).ok()?;
|
||||
|
||||
if meta.is_file() {
|
||||
if Project::is_project_file(path) {
|
||||
Some(path.to_path_buf())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let child_path = path.join(PROJECT_FILENAME);
|
||||
let child_meta = fs::metadata(&child_path).ok()?;
|
||||
|
||||
if child_meta.is_file() {
|
||||
Some(child_path)
|
||||
} else {
|
||||
// This is a folder with the same name as a Rojo default project
|
||||
// file.
|
||||
//
|
||||
// That's pretty weird, but we can roll with it.
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_exact(project_file_location: &Path) -> anyhow::Result<Self> {
|
||||
let contents = fs::read_to_string(project_file_location)?;
|
||||
|
||||
let mut project: Project = serde_json::from_str(&contents).with_context(|| {
|
||||
format!(
|
||||
"Error parsing Rojo project at {}",
|
||||
project_file_location.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
project.file_location = project_file_location.to_path_buf();
|
||||
project.check_compatibility();
|
||||
|
||||
Ok(project)
|
||||
}
|
||||
|
||||
/// Checks if there are any compatibility issues with this project file and
|
||||
/// warns the user if there are any.
|
||||
fn check_compatibility(&self) {
|
||||
self.tree.validate_reserved_names();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct OptionalPathNode {
|
||||
#[serde(serialize_with = "crate::path_serializer::serialize_absolute")]
|
||||
pub optional: PathBuf,
|
||||
}
|
||||
|
||||
impl OptionalPathNode {
|
||||
pub fn new(optional: PathBuf) -> Self {
|
||||
OptionalPathNode { optional }
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a path that is either optional or required
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum PathNode {
|
||||
Required(#[serde(serialize_with = "crate::path_serializer::serialize_absolute")] PathBuf),
|
||||
Optional(OptionalPathNode),
|
||||
}
|
||||
|
||||
impl PathNode {
|
||||
pub fn path(&self) -> &Path {
|
||||
match self {
|
||||
PathNode::Required(pathbuf) => &pathbuf,
|
||||
PathNode::Optional(OptionalPathNode { optional }) => &optional,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes an instance and its descendants in a project.
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct ProjectNode {
|
||||
/// If set, defines the ClassName of the described instance.
|
||||
///
|
||||
/// `$className` MUST be set if `$path` is not set.
|
||||
///
|
||||
/// `$className` CANNOT be set if `$path` is set and the instance described
|
||||
/// by that path has a ClassName other than Folder.
|
||||
#[serde(rename = "$className", skip_serializing_if = "Option::is_none")]
|
||||
pub class_name: Option<String>,
|
||||
|
||||
/// Contains all of the children of the described instance.
|
||||
#[serde(flatten)]
|
||||
pub children: BTreeMap<String, ProjectNode>,
|
||||
|
||||
/// The properties that will be assigned to the resulting instance.
|
||||
#[serde(
|
||||
rename = "$properties",
|
||||
default,
|
||||
skip_serializing_if = "HashMap::is_empty"
|
||||
)]
|
||||
pub properties: HashMap<String, UnresolvedValue>,
|
||||
|
||||
/// Defines the behavior when Rojo encounters unknown instances in Roblox
|
||||
/// Studio during live sync. `$ignoreUnknownInstances` should be considered
|
||||
/// a large hammer and used with care.
|
||||
///
|
||||
/// If set to `true`, those instances will be left alone. This may cause
|
||||
/// issues when files that turn into instances are removed while Rojo is not
|
||||
/// running.
|
||||
///
|
||||
/// If set to `false`, Rojo will destroy any instances it does not
|
||||
/// recognize.
|
||||
///
|
||||
/// If unset, its default value depends on other settings:
|
||||
/// - If `$path` is not set, defaults to `true`
|
||||
/// - If `$path` is set, defaults to `false`
|
||||
#[serde(
|
||||
rename = "$ignoreUnknownInstances",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub ignore_unknown_instances: Option<bool>,
|
||||
|
||||
/// Defines that this instance should come from the given file path. This
|
||||
/// path can point to any file type supported by Rojo, including Lua files
|
||||
/// (`.lua`), Roblox models (`.rbxm`, `.rbxmx`), and localization table
|
||||
/// spreadsheets (`.csv`).
|
||||
#[serde(rename = "$path", skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<PathNode>,
|
||||
}
|
||||
|
||||
impl ProjectNode {
|
||||
fn validate_reserved_names(&self) {
|
||||
for (name, child) in &self.children {
|
||||
if name.starts_with('$') {
|
||||
log::warn!(
|
||||
"Keys starting with '$' are reserved by Rojo to ensure forward compatibility."
|
||||
);
|
||||
log::warn!(
|
||||
"This project uses the key '{}', which should be renamed.",
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
child.validate_reserved_names();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn path_node_required() {
|
||||
let path_node: PathNode = serde_json::from_str(r#""src""#).unwrap();
|
||||
assert_eq!(path_node, PathNode::Required(PathBuf::from("src")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_node_optional() {
|
||||
let path_node: PathNode = serde_json::from_str(r#"{ "optional": "src" }"#).unwrap();
|
||||
assert_eq!(
|
||||
path_node,
|
||||
PathNode::Optional(OptionalPathNode::new(PathBuf::from("src")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_node_required() {
|
||||
let project_node: ProjectNode = serde_json::from_str(
|
||||
r#"{
|
||||
"$path": "src"
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
project_node.path,
|
||||
Some(PathNode::Required(PathBuf::from("src")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_node_optional() {
|
||||
let project_node: ProjectNode = serde_json::from_str(
|
||||
r#"{
|
||||
"$path": { "optional": "src" }
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
project_node.path,
|
||||
Some(PathNode::Optional(OptionalPathNode::new(PathBuf::from(
|
||||
"src"
|
||||
))))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_node_none() {
|
||||
let project_node: ProjectNode = serde_json::from_str(
|
||||
r#"{
|
||||
"$className": "Folder"
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(project_node.path, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_node_optional_serialize_absolute() {
|
||||
let project_node: ProjectNode = serde_json::from_str(
|
||||
r#"{
|
||||
"$path": { "optional": "..\\src" }
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let serialized = serde_json::to_string(&project_node).unwrap();
|
||||
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_node_optional_serialize_absolute_no_change() {
|
||||
let project_node: ProjectNode = serde_json::from_str(
|
||||
r#"{
|
||||
"$path": { "optional": "../src" }
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let serialized = serde_json::to_string(&project_node).unwrap();
|
||||
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_node_optional_serialize_optional() {
|
||||
let project_node: ProjectNode = serde_json::from_str(
|
||||
r#"{
|
||||
"$path": "..\\src"
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let serialized = serde_json::to_string(&project_node).unwrap();
|
||||
assert_eq!(serialized, r#"{"$path":"../src"}"#);
|
||||
}
|
||||
}
|
||||
294
crates/rojo-project/src/resolution.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
use std::borrow::Borrow;
|
||||
|
||||
use anyhow::format_err;
|
||||
use rbx_dom_weak::types::{
|
||||
CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2, Vector3,
|
||||
};
|
||||
use rbx_reflection::{DataType, PropertyDescriptor};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A user-friendly version of `Variant` that supports specifying ambiguous
|
||||
/// values. Ambiguous values need a reflection database to be resolved to a
|
||||
/// usable value.
|
||||
///
|
||||
/// This type is used in Rojo projects and JSON models to make specifying the
|
||||
/// most common types of properties, like strings or vectors, much easier.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum UnresolvedValue {
|
||||
FullyQualified(Variant),
|
||||
Ambiguous(AmbiguousValue),
|
||||
}
|
||||
|
||||
impl UnresolvedValue {
|
||||
pub fn resolve(self, class_name: &str, prop_name: &str) -> anyhow::Result<Variant> {
|
||||
match self {
|
||||
UnresolvedValue::FullyQualified(full) => Ok(full),
|
||||
UnresolvedValue::Ambiguous(partial) => partial.resolve(class_name, prop_name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum AmbiguousValue {
|
||||
Bool(bool),
|
||||
String(String),
|
||||
StringArray(Vec<String>),
|
||||
Number(f64),
|
||||
Array2([f64; 2]),
|
||||
Array3([f64; 3]),
|
||||
Array4([f64; 4]),
|
||||
Array12([f64; 12]),
|
||||
}
|
||||
|
||||
impl AmbiguousValue {
|
||||
pub fn resolve(self, class_name: &str, prop_name: &str) -> anyhow::Result<Variant> {
|
||||
let property = find_descriptor(class_name, prop_name)
|
||||
.ok_or_else(|| format_err!("Unknown property {}.{}", class_name, prop_name))?;
|
||||
|
||||
match &property.data_type {
|
||||
DataType::Enum(enum_name) => {
|
||||
let database = rbx_reflection_database::get();
|
||||
|
||||
let enum_descriptor = database.enums.get(enum_name).ok_or_else(|| {
|
||||
format_err!("Unknown enum {}. This is a Rojo bug!", enum_name)
|
||||
})?;
|
||||
|
||||
let error = |what: &str| {
|
||||
let mut all_values = enum_descriptor
|
||||
.items
|
||||
.keys()
|
||||
.map(|value| value.borrow())
|
||||
.collect::<Vec<_>>();
|
||||
all_values.sort();
|
||||
|
||||
let examples = nonexhaustive_list(&all_values);
|
||||
|
||||
format_err!(
|
||||
"Invalid value for property {}.{}. Got {} but \
|
||||
expected a member of the {} enum such as {}",
|
||||
class_name,
|
||||
prop_name,
|
||||
what,
|
||||
enum_name,
|
||||
examples,
|
||||
)
|
||||
};
|
||||
|
||||
let value = match self {
|
||||
AmbiguousValue::String(value) => value,
|
||||
unresolved => return Err(error(unresolved.describe())),
|
||||
};
|
||||
|
||||
let resolved = enum_descriptor
|
||||
.items
|
||||
.get(value.as_str())
|
||||
.ok_or_else(|| error(value.as_str()))?;
|
||||
|
||||
Ok(Enum::from_u32(*resolved).into())
|
||||
}
|
||||
DataType::Value(variant_ty) => match (variant_ty, self) {
|
||||
(VariantType::Bool, AmbiguousValue::Bool(value)) => Ok(value.into()),
|
||||
|
||||
(VariantType::Float32, AmbiguousValue::Number(value)) => Ok((value as f32).into()),
|
||||
(VariantType::Float64, AmbiguousValue::Number(value)) => Ok(value.into()),
|
||||
(VariantType::Int32, AmbiguousValue::Number(value)) => Ok((value as i32).into()),
|
||||
(VariantType::Int64, AmbiguousValue::Number(value)) => Ok((value as i64).into()),
|
||||
|
||||
(VariantType::String, AmbiguousValue::String(value)) => Ok(value.into()),
|
||||
(VariantType::Tags, AmbiguousValue::StringArray(value)) => {
|
||||
Ok(Tags::from(value).into())
|
||||
}
|
||||
(VariantType::Content, AmbiguousValue::String(value)) => {
|
||||
Ok(Content::from(value).into())
|
||||
}
|
||||
|
||||
(VariantType::Vector2, AmbiguousValue::Array2(value)) => {
|
||||
Ok(Vector2::new(value[0] as f32, value[1] as f32).into())
|
||||
}
|
||||
|
||||
(VariantType::Vector3, AmbiguousValue::Array3(value)) => {
|
||||
Ok(Vector3::new(value[0] as f32, value[1] as f32, value[2] as f32).into())
|
||||
}
|
||||
|
||||
(VariantType::Color3, AmbiguousValue::Array3(value)) => {
|
||||
Ok(Color3::new(value[0] as f32, value[1] as f32, value[2] as f32).into())
|
||||
}
|
||||
|
||||
(VariantType::CFrame, AmbiguousValue::Array12(value)) => {
|
||||
let value = value.map(|v| v as f32);
|
||||
let pos = Vector3::new(value[0], value[1], value[2]);
|
||||
let orientation = Matrix3::new(
|
||||
Vector3::new(value[3], value[4], value[5]),
|
||||
Vector3::new(value[6], value[7], value[8]),
|
||||
Vector3::new(value[9], value[10], value[11]),
|
||||
);
|
||||
|
||||
Ok(CFrame::new(pos, orientation).into())
|
||||
}
|
||||
|
||||
(_, unresolved) => Err(format_err!(
|
||||
"Wrong type of value for property {}.{}. Expected {:?}, got {}",
|
||||
class_name,
|
||||
prop_name,
|
||||
variant_ty,
|
||||
unresolved.describe(),
|
||||
)),
|
||||
},
|
||||
_ => Err(format_err!(
|
||||
"Unknown data type for property {}.{}",
|
||||
class_name,
|
||||
prop_name
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn describe(&self) -> &'static str {
|
||||
match self {
|
||||
AmbiguousValue::Bool(_) => "a bool",
|
||||
AmbiguousValue::String(_) => "a string",
|
||||
AmbiguousValue::StringArray(_) => "an array of strings",
|
||||
AmbiguousValue::Number(_) => "a number",
|
||||
AmbiguousValue::Array2(_) => "an array of two numbers",
|
||||
AmbiguousValue::Array3(_) => "an array of three numbers",
|
||||
AmbiguousValue::Array4(_) => "an array of four numbers",
|
||||
AmbiguousValue::Array12(_) => "an array of twelve numbers",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_descriptor(
|
||||
class_name: &str,
|
||||
prop_name: &str,
|
||||
) -> Option<&'static PropertyDescriptor<'static>> {
|
||||
let database = rbx_reflection_database::get();
|
||||
let mut current_class_name = class_name;
|
||||
|
||||
loop {
|
||||
let class = database.classes.get(current_class_name)?;
|
||||
if let Some(descriptor) = class.properties.get(prop_name) {
|
||||
return Some(descriptor);
|
||||
}
|
||||
|
||||
current_class_name = class.superclass.as_deref()?;
|
||||
}
|
||||
}
|
||||
|
||||
/// Outputs a string containing up to MAX_ITEMS entries from the given list. If
|
||||
/// there are more than MAX_ITEMS items, the number of remaining items will be
|
||||
/// listed.
|
||||
fn nonexhaustive_list(values: &[&str]) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
const MAX_ITEMS: usize = 8;
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
let last_index = values.len() - 1;
|
||||
let main_length = last_index.min(9);
|
||||
|
||||
let main_list = &values[..main_length];
|
||||
for value in main_list {
|
||||
output.push_str(value);
|
||||
output.push_str(", ");
|
||||
}
|
||||
|
||||
if values.len() > MAX_ITEMS {
|
||||
write!(output, "or {} more", values.len() - main_length).unwrap();
|
||||
} else {
|
||||
output.push_str("or ");
|
||||
output.push_str(values[values.len() - 1]);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
fn resolve(class: &str, prop: &str, json_value: &str) -> Variant {
|
||||
let unresolved: UnresolvedValue = serde_json::from_str(json_value).unwrap();
|
||||
unresolved.resolve(class, prop).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bools() {
|
||||
assert_eq!(resolve("BoolValue", "Value", "false"), Variant::Bool(false));
|
||||
|
||||
// Script.Disabled is inherited from BaseScript
|
||||
assert_eq!(resolve("Script", "Disabled", "true"), Variant::Bool(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strings() {
|
||||
// String literals can stay as strings
|
||||
assert_eq!(
|
||||
resolve("StringValue", "Value", "\"Hello!\""),
|
||||
Variant::String("Hello!".into()),
|
||||
);
|
||||
|
||||
// String literals can also turn into Content
|
||||
assert_eq!(
|
||||
resolve("Sky", "MoonTextureId", "\"rbxassetid://12345\""),
|
||||
Variant::Content("rbxassetid://12345".into()),
|
||||
);
|
||||
|
||||
// What about BinaryString values? For forward-compatibility reasons, we
|
||||
// don't support any shorthands for BinaryString.
|
||||
//
|
||||
// assert_eq!(
|
||||
// resolve("Folder", "Tags", "\"a\\u0000b\\u0000c\""),
|
||||
// Variant::BinaryString(b"a\0b\0c".to_vec().into()),
|
||||
// );
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers() {
|
||||
assert_eq!(
|
||||
resolve("Part", "CollisionGroupId", "123"),
|
||||
Variant::Int32(123),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
resolve("Folder", "SourceAssetId", "532413"),
|
||||
Variant::Int64(532413),
|
||||
);
|
||||
|
||||
assert_eq!(resolve("Part", "Transparency", "1"), Variant::Float32(1.0));
|
||||
assert_eq!(resolve("NumberValue", "Value", "1"), Variant::Float64(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vectors() {
|
||||
assert_eq!(
|
||||
resolve("ParticleEmitter", "SpreadAngle", "[1, 2]"),
|
||||
Variant::Vector2(Vector2::new(1.0, 2.0)),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
resolve("Part", "Position", "[4, 5, 6]"),
|
||||
Variant::Vector3(Vector3::new(4.0, 5.0, 6.0)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colors() {
|
||||
assert_eq!(
|
||||
resolve("Part", "Color", "[1, 1, 1]"),
|
||||
Variant::Color3(Color3::new(1.0, 1.0, 1.0)),
|
||||
);
|
||||
|
||||
// There aren't any user-facing Color3uint8 properties. If there are
|
||||
// some, we should treat them the same in the future.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enums() {
|
||||
assert_eq!(
|
||||
resolve("Lighting", "Technology", "\"Voxel\""),
|
||||
Variant::Enum(Enum::from_u32(1)),
|
||||
);
|
||||
}
|
||||
}
|
||||
4
foreman.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[tools]
|
||||
rojo = { source = "rojo-rbx/rojo", version = "7.1.1" }
|
||||
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }
|
||||
selene = { source = "Kampfkarren/selene", version = "0.17.0" }
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "Rojo",
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
"Plugin": {
|
||||
"$path": "plugin/src"
|
||||
},
|
||||
"Packages": {
|
||||
"$path": "plugin/Packages",
|
||||
"Log": {
|
||||
"$path": "plugin/log"
|
||||
},
|
||||
"Http": {
|
||||
"$path": "plugin/http"
|
||||
},
|
||||
"Fmt": {
|
||||
"$path": "plugin/fmt"
|
||||
},
|
||||
"RbxDom": {
|
||||
"$path": "plugin/rbx_dom_lua"
|
||||
}
|
||||
},
|
||||
"Version": {
|
||||
"$path": "plugin/Version.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
7.6.0
|
||||
33
plugin/default.project.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "Rojo",
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
"Plugin": {
|
||||
"$path": "src"
|
||||
},
|
||||
"Log": {
|
||||
"$path": "log"
|
||||
},
|
||||
"Http": {
|
||||
"$path": "http"
|
||||
},
|
||||
"Fmt": {
|
||||
"$path": "fmt"
|
||||
},
|
||||
"RbxDom": {
|
||||
"$path": "rbx_dom_lua"
|
||||
},
|
||||
"Roact": {
|
||||
"$path": "modules/roact/src"
|
||||
},
|
||||
"Promise": {
|
||||
"$path": "modules/promise/lib"
|
||||
},
|
||||
"t": {
|
||||
"$path": "modules/t/lib"
|
||||
},
|
||||
"Flipper": {
|
||||
"$path": "modules/flipper/src"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ local function debugImpl(buffer, value, extendedForm)
|
||||
elseif valueType == "table" then
|
||||
local valueMeta = getmetatable(value)
|
||||
|
||||
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
|
||||
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
|
||||
-- This type implement's the metamethod we made up to line up with
|
||||
-- Rust's 'Debug' trait.
|
||||
|
||||
@@ -242,4 +242,4 @@ return {
|
||||
debugOutputBuffer = debugOutputBuffer,
|
||||
fmt = fmt,
|
||||
debugify = debugify,
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@ Error.__index = Error
|
||||
|
||||
Error.Kind = {
|
||||
HttpNotEnabled = {
|
||||
message = "Rojo requires HTTP access, which is not enabled.\n"
|
||||
.. "Check your game settings, located in the 'Home' tab of Studio.",
|
||||
message = "Rojo requires HTTP access, which is not enabled.\n" ..
|
||||
"Check your game settings, located in the 'Home' tab of Studio.",
|
||||
},
|
||||
ConnectFailed = {
|
||||
message = "Couldn't connect to the Rojo server.\n"
|
||||
.. "Make sure the server is running — use 'rojo serve' to run it!",
|
||||
message = "Couldn't connect to the Rojo server.\n" ..
|
||||
"Make sure the server is running — use 'rojo serve' to run it!",
|
||||
},
|
||||
Timeout = {
|
||||
message = "HTTP request timed out.",
|
||||
@@ -63,13 +63,4 @@ function Error.fromRobloxErrorString(message)
|
||||
return Error.new(Error.Kind.Unknown, message)
|
||||
end
|
||||
|
||||
function Error.fromResponse(response)
|
||||
local lower = (response.body or ""):lower()
|
||||
if response.code == 408 or response.code == 504 or lower:find("timed? ?out") then
|
||||
return Error.new(Error.Kind.Timeout)
|
||||
end
|
||||
|
||||
return Error.new(Error.Kind.Unknown, string.format("%s: %s", tostring(response.code), tostring(response.body)))
|
||||
end
|
||||
|
||||
return Error
|
||||
|
||||
@@ -31,4 +31,4 @@ function Response:json()
|
||||
return HttpService:JSONDecode(self.body)
|
||||
end
|
||||
|
||||
return Response
|
||||
return Response
|
||||
@@ -30,13 +30,8 @@ local function performRequest(requestParams)
|
||||
end)
|
||||
|
||||
if success then
|
||||
Log.trace("Request {} success, response {:#?}", requestId, response)
|
||||
local httpResponse = HttpResponse.fromRobloxResponse(response)
|
||||
if httpResponse:isSuccess() then
|
||||
resolve(httpResponse)
|
||||
else
|
||||
reject(HttpError.fromResponse(httpResponse))
|
||||
end
|
||||
Log.trace("Request {} success, status code {}", requestId, response.StatusCode)
|
||||
resolve(HttpResponse.fromRobloxResponse(response))
|
||||
else
|
||||
Log.trace("Request {} failure: {:?}", requestId, response)
|
||||
reject(HttpError.fromRobloxErrorString(response))
|
||||
@@ -68,4 +63,4 @@ function Http.jsonDecode(source)
|
||||
return HttpService:JSONDecode(source)
|
||||
end
|
||||
|
||||
return Http
|
||||
return Http
|
||||
@@ -2,4 +2,4 @@ return function()
|
||||
it("should load", function()
|
||||
require(script.Parent)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -57,4 +57,4 @@ function Log.error(template, ...)
|
||||
error(Fmt.fmt(template, ...))
|
||||
end
|
||||
|
||||
return Log
|
||||
return Log
|
||||
@@ -2,4 +2,4 @@ return function()
|
||||
it("should load", function()
|
||||
require(script.Parent)
|
||||
end)
|
||||
end
|
||||
end
|
||||
1
plugin/modules/flipper
Submodule
1
plugin/modules/promise
Submodule
1
plugin/modules/roact
Submodule
1
plugin/modules/t
Submodule
1
plugin/modules/testez
Submodule
@@ -20,54 +20,11 @@ local function serializeFloat(value)
|
||||
return value
|
||||
end
|
||||
|
||||
local ALL_AXES = { "X", "Y", "Z" }
|
||||
local ALL_FACES = { "Right", "Top", "Back", "Left", "Bottom", "Front" }
|
||||
|
||||
local EncodedValue = {}
|
||||
local ALL_AXES = {"X", "Y", "Z"}
|
||||
local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"}
|
||||
|
||||
local 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 = {
|
||||
fromPod = function(pod)
|
||||
local axes = {}
|
||||
@@ -117,7 +74,6 @@ types = {
|
||||
local pos = pod.position
|
||||
local orient = pod.orientation
|
||||
|
||||
--stylua: ignore
|
||||
return CFrame.new(
|
||||
pos[1], pos[2], pos[3],
|
||||
orient[1][1], orient[1][2], orient[1][3],
|
||||
@@ -127,14 +83,17 @@ types = {
|
||||
end,
|
||||
|
||||
toPod = function(roblox)
|
||||
local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = roblox:GetComponents()
|
||||
local x, y, z,
|
||||
r00, r01, r02,
|
||||
r10, r11, r12,
|
||||
r20, r21, r22 = roblox:GetComponents()
|
||||
|
||||
return {
|
||||
position = { x, y, z },
|
||||
position = {x, y, z},
|
||||
orientation = {
|
||||
{ r00, r01, r02 },
|
||||
{ r10, r11, r12 },
|
||||
{ r20, r21, r22 },
|
||||
{r00, r01, r02},
|
||||
{r10, r11, r12},
|
||||
{r20, r21, r22},
|
||||
},
|
||||
}
|
||||
end,
|
||||
@@ -144,7 +103,7 @@ types = {
|
||||
fromPod = unpackDecoder(Color3.new),
|
||||
|
||||
toPod = function(roblox)
|
||||
return { roblox.r, roblox.g, roblox.b }
|
||||
return {roblox.r, roblox.g, roblox.b}
|
||||
end,
|
||||
},
|
||||
|
||||
@@ -165,7 +124,10 @@ types = {
|
||||
local keypoints = {}
|
||||
|
||||
for index, keypoint in ipairs(pod.keypoints) do
|
||||
keypoints[index] = ColorSequenceKeypoint.new(keypoint.time, types.Color3.fromPod(keypoint.color))
|
||||
keypoints[index] = ColorSequenceKeypoint.new(
|
||||
keypoint.time,
|
||||
types.Color3.fromPod(keypoint.color)
|
||||
)
|
||||
end
|
||||
|
||||
return ColorSequence.new(keypoints)
|
||||
@@ -188,38 +150,6 @@ types = {
|
||||
},
|
||||
|
||||
Content = {
|
||||
fromPod = function(pod): Content
|
||||
if type(pod) == "string" then
|
||||
if pod == "None" then
|
||||
return Content.none
|
||||
else
|
||||
error(`unexpected Content value '{pod}'`)
|
||||
end
|
||||
else
|
||||
local ty, value = next(pod)
|
||||
if ty == "Uri" then
|
||||
return Content.fromUri(value)
|
||||
elseif ty == "Object" then
|
||||
error("Object deserializing is not currently implemented")
|
||||
else
|
||||
error(`Unknown Content type '{ty}' (could not deserialize)`)
|
||||
end
|
||||
end
|
||||
end,
|
||||
toPod = function(roblox: Content)
|
||||
if roblox.SourceType == Enum.ContentSourceType.None then
|
||||
return "None"
|
||||
elseif roblox.SourceType == Enum.ContentSourceType.Uri then
|
||||
return { Uri = roblox.Uri }
|
||||
elseif roblox.SourceType == Enum.ContentSourceType.Object then
|
||||
error("Object serializing is not currently implemented")
|
||||
else
|
||||
error(`Unknown Content type '{roblox.SourceType} (could not serialize)`)
|
||||
end
|
||||
end,
|
||||
},
|
||||
|
||||
ContentId = {
|
||||
fromPod = identity,
|
||||
toPod = identity,
|
||||
},
|
||||
@@ -237,19 +167,6 @@ types = {
|
||||
end,
|
||||
},
|
||||
|
||||
EnumItem = {
|
||||
fromPod = function(pod)
|
||||
return Enum[pod.type]:FromValue(pod.value)
|
||||
end,
|
||||
|
||||
toPod = function(roblox)
|
||||
return {
|
||||
type = tostring(roblox.EnumType),
|
||||
value = roblox.Value,
|
||||
}
|
||||
end,
|
||||
},
|
||||
|
||||
Faces = {
|
||||
fromPod = function(pod)
|
||||
local faces = {}
|
||||
@@ -284,23 +201,6 @@ types = {
|
||||
toPod = serializeFloat,
|
||||
},
|
||||
|
||||
Font = {
|
||||
fromPod = function(pod)
|
||||
return Font.new(
|
||||
pod.family,
|
||||
if pod.weight ~= nil then Enum.FontWeight[pod.weight] else nil,
|
||||
if pod.style ~= nil then Enum.FontStyle[pod.style] else nil
|
||||
)
|
||||
end,
|
||||
toPod = function(roblox)
|
||||
return {
|
||||
family = roblox.Family,
|
||||
weight = roblox.Weight.Name,
|
||||
style = roblox.Style.Name,
|
||||
}
|
||||
end,
|
||||
},
|
||||
|
||||
Int32 = {
|
||||
fromPod = identity,
|
||||
toPod = identity,
|
||||
@@ -311,32 +211,11 @@ types = {
|
||||
toPod = identity,
|
||||
},
|
||||
|
||||
MaterialColors = {
|
||||
fromPod = function(pod: { [string]: { number } })
|
||||
local real = {}
|
||||
for name, color in pod do
|
||||
real[Enum.Material[name]] = Color3.fromRGB(color[1], color[2], color[3])
|
||||
end
|
||||
return real
|
||||
end,
|
||||
toPod = function(roblox: { [Enum.Material]: Color3 })
|
||||
local pod = {}
|
||||
for material, color in roblox do
|
||||
pod[material.Name] = {
|
||||
math.round(math.clamp(color.R, 0, 1) * 255),
|
||||
math.round(math.clamp(color.G, 0, 1) * 255),
|
||||
math.round(math.clamp(color.B, 0, 1) * 255),
|
||||
}
|
||||
end
|
||||
return pod
|
||||
end,
|
||||
},
|
||||
|
||||
NumberRange = {
|
||||
fromPod = unpackDecoder(NumberRange.new),
|
||||
|
||||
toPod = function(roblox)
|
||||
return { roblox.Min, roblox.Max }
|
||||
return {roblox.Min, roblox.Max}
|
||||
end,
|
||||
},
|
||||
|
||||
@@ -345,12 +224,11 @@ types = {
|
||||
local keypoints = {}
|
||||
|
||||
for index, keypoint in ipairs(pod.keypoints) do
|
||||
-- TODO: Add a test for NaN or Infinity values and envelopes
|
||||
-- Right now it isn't possible because it'd fail the roundtrip.
|
||||
-- It's more important that it works right now, though.
|
||||
local value = keypoint.value or 0
|
||||
local envelope = keypoint.envelope or 0
|
||||
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, value, envelope)
|
||||
keypoints[index] = NumberSequenceKeypoint.new(
|
||||
keypoint.time,
|
||||
keypoint.value,
|
||||
keypoint.envelope
|
||||
)
|
||||
end
|
||||
|
||||
return NumberSequence.new(keypoints)
|
||||
@@ -378,26 +256,13 @@ types = {
|
||||
if pod == "Default" then
|
||||
return nil
|
||||
else
|
||||
-- Passing `nil` instead of not passing anything gives
|
||||
-- different results, so we have to branch here.
|
||||
if pod.acousticAbsorption then
|
||||
return (PhysicalProperties.new :: any)(
|
||||
pod.density,
|
||||
pod.friction,
|
||||
pod.elasticity,
|
||||
pod.frictionWeight,
|
||||
pod.elasticityWeight,
|
||||
pod.acousticAbsorption
|
||||
)
|
||||
else
|
||||
return PhysicalProperties.new(
|
||||
pod.density,
|
||||
pod.friction,
|
||||
pod.elasticity,
|
||||
pod.frictionWeight,
|
||||
pod.elasticityWeight
|
||||
)
|
||||
end
|
||||
return PhysicalProperties.new(
|
||||
pod.density,
|
||||
pod.friction,
|
||||
pod.elasticity,
|
||||
pod.frictionWeight,
|
||||
pod.elasticityWeight
|
||||
)
|
||||
end
|
||||
end,
|
||||
|
||||
@@ -411,7 +276,6 @@ types = {
|
||||
elasticity = roblox.Elasticity,
|
||||
frictionWeight = roblox.FrictionWeight,
|
||||
elasticityWeight = roblox.ElasticityWeight,
|
||||
acousticAbsorption = roblox.AcousticAbsorption,
|
||||
}
|
||||
end
|
||||
end,
|
||||
@@ -419,7 +283,10 @@ types = {
|
||||
|
||||
Ray = {
|
||||
fromPod = function(pod)
|
||||
return Ray.new(types.Vector3.fromPod(pod.origin), types.Vector3.fromPod(pod.direction))
|
||||
return Ray.new(
|
||||
types.Vector3.fromPod(pod.origin),
|
||||
types.Vector3.fromPod(pod.direction)
|
||||
)
|
||||
end,
|
||||
|
||||
toPod = function(roblox)
|
||||
@@ -432,7 +299,10 @@ types = {
|
||||
|
||||
Rect = {
|
||||
fromPod = function(pod)
|
||||
return Rect.new(types.Vector2.fromPod(pod[1]), types.Vector2.fromPod(pod[2]))
|
||||
return Rect.new(
|
||||
types.Vector2.fromPod(pod[1]),
|
||||
types.Vector2.fromPod(pod[2])
|
||||
)
|
||||
end,
|
||||
|
||||
toPod = function(roblox)
|
||||
@@ -444,28 +314,31 @@ types = {
|
||||
},
|
||||
|
||||
Ref = {
|
||||
fromPod = function(_)
|
||||
fromPod = function(_pod)
|
||||
error("Ref cannot be decoded on its own")
|
||||
end,
|
||||
|
||||
toPod = function(_)
|
||||
toPod = function(_roblox)
|
||||
error("Ref can not be encoded on its own")
|
||||
end,
|
||||
},
|
||||
|
||||
Region3 = {
|
||||
fromPod = function(_)
|
||||
fromPod = function(pod)
|
||||
error("Region3 is not implemented")
|
||||
end,
|
||||
|
||||
toPod = function(_)
|
||||
toPod = function(roblox)
|
||||
error("Region3 is not implemented")
|
||||
end,
|
||||
},
|
||||
|
||||
Region3int16 = {
|
||||
fromPod = function(pod)
|
||||
return Region3int16.new(types.Vector3int16.fromPod(pod[1]), types.Vector3int16.fromPod(pod[2]))
|
||||
return Region3int16.new(
|
||||
types.Vector3int16.fromPod(pod[1]),
|
||||
types.Vector3int16.fromPod(pod[2])
|
||||
)
|
||||
end,
|
||||
|
||||
toPod = function(roblox)
|
||||
@@ -477,11 +350,11 @@ types = {
|
||||
},
|
||||
|
||||
SharedString = {
|
||||
fromPod = function(_pod)
|
||||
fromPod = function(pod)
|
||||
error("SharedString is not supported")
|
||||
end,
|
||||
|
||||
toPod = function(_roblox)
|
||||
toPod = function(roblox)
|
||||
error("SharedString is not supported")
|
||||
end,
|
||||
},
|
||||
@@ -495,13 +368,16 @@ types = {
|
||||
fromPod = unpackDecoder(UDim.new),
|
||||
|
||||
toPod = function(roblox)
|
||||
return { roblox.Scale, roblox.Offset }
|
||||
return {roblox.Scale, roblox.Offset}
|
||||
end,
|
||||
},
|
||||
|
||||
UDim2 = {
|
||||
fromPod = function(pod)
|
||||
return UDim2.new(types.UDim.fromPod(pod[1]), types.UDim.fromPod(pod[2]))
|
||||
return UDim2.new(
|
||||
types.UDim.fromPod(pod[1]),
|
||||
types.UDim.fromPod(pod[2])
|
||||
)
|
||||
end,
|
||||
|
||||
toPod = function(roblox)
|
||||
@@ -532,7 +408,7 @@ types = {
|
||||
fromPod = unpackDecoder(Vector2int16.new),
|
||||
|
||||
toPod = function(roblox)
|
||||
return { roblox.X, roblox.Y }
|
||||
return {roblox.X, roblox.Y}
|
||||
end,
|
||||
},
|
||||
|
||||
@@ -552,37 +428,16 @@ types = {
|
||||
fromPod = unpackDecoder(Vector3int16.new),
|
||||
|
||||
toPod = function(roblox)
|
||||
return { roblox.X, roblox.Y, roblox.Z }
|
||||
return {roblox.X, roblox.Y, roblox.Z}
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
types.OptionalCFrame = {
|
||||
fromPod = function(pod)
|
||||
if pod == nil then
|
||||
return nil
|
||||
else
|
||||
return types.CFrame.fromPod(pod)
|
||||
end
|
||||
end,
|
||||
|
||||
toPod = function(roblox)
|
||||
if roblox == nil then
|
||||
return nil
|
||||
else
|
||||
return types.CFrame.toPod(roblox)
|
||||
end
|
||||
end,
|
||||
}
|
||||
local EncodedValue = {}
|
||||
|
||||
function EncodedValue.decode(encodedValue)
|
||||
local ty, value = next(encodedValue)
|
||||
|
||||
if ty == nil then
|
||||
-- If the encoded pair is empty, assume it is an unoccupied optional value
|
||||
return true, nil
|
||||
end
|
||||
|
||||
local typeImpl = types[ty]
|
||||
if typeImpl == nil then
|
||||
return false, "Couldn't decode value " .. tostring(ty)
|
||||
@@ -604,19 +459,4 @@ function EncodedValue.encode(rbxValue, propertyType)
|
||||
}
|
||||
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
|
||||
|
||||
72
plugin/rbx_dom_lua/EncodedValue.spec.lua
Normal file
@@ -0,0 +1,72 @@
|
||||
return function()
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local EncodedValue = require(script.Parent.EncodedValue)
|
||||
local allValues = require(script.Parent.allValues)
|
||||
|
||||
local function deepEq(a, b)
|
||||
if typeof(a) ~= typeof(b) then
|
||||
return false
|
||||
end
|
||||
|
||||
local ty = typeof(a)
|
||||
|
||||
if ty == "table" then
|
||||
local visited = {}
|
||||
|
||||
for key, valueA in pairs(a) do
|
||||
visited[key] = true
|
||||
|
||||
if not deepEq(valueA, b[key]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
for key, valueB in pairs(b) do
|
||||
if visited[key] then
|
||||
continue
|
||||
end
|
||||
|
||||
if not deepEq(valueB, a[key]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
else
|
||||
return a == b
|
||||
end
|
||||
end
|
||||
|
||||
local extraAssertions = {
|
||||
CFrame = function(value)
|
||||
expect(value).to.equal(CFrame.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12))
|
||||
end,
|
||||
}
|
||||
|
||||
for testName, testEntry in pairs(allValues) do
|
||||
it("round trip " .. testName, function()
|
||||
local ok, decoded = EncodedValue.decode(testEntry.value)
|
||||
assert(ok, decoded)
|
||||
|
||||
if extraAssertions[testName] ~= nil then
|
||||
extraAssertions[testName](decoded)
|
||||
end
|
||||
|
||||
local ok, encoded = EncodedValue.encode(decoded, testEntry.ty)
|
||||
assert(ok, encoded)
|
||||
|
||||
if not deepEq(encoded, testEntry.value) then
|
||||
local expected = HttpService:JSONEncode(testEntry.value)
|
||||
local actual = HttpService:JSONEncode(encoded)
|
||||
|
||||
local message = string.format(
|
||||
"Round-trip results did not match.\nExpected:\n%s\nActual:\n%s",
|
||||
expected, actual
|
||||
)
|
||||
|
||||
error(message)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -5,7 +5,6 @@ Error.Kind = {
|
||||
UnknownProperty = "UnknownProperty",
|
||||
PropertyNotReadable = "PropertyNotReadable",
|
||||
PropertyNotWritable = "PropertyNotWritable",
|
||||
CannotParseBinaryString = "CannotParseBinaryString",
|
||||
Roblox = "Roblox",
|
||||
}
|
||||
|
||||
@@ -26,4 +25,4 @@ function Error:__tostring()
|
||||
return ("Error(%s: %s)"):format(self.kind, tostring(self.extra))
|
||||
end
|
||||
|
||||
return Error
|
||||
return Error
|
||||
@@ -53,11 +53,6 @@ function PropertyDescriptor:read(instance)
|
||||
end
|
||||
|
||||
if self.scriptability == "Custom" then
|
||||
if customProperties[self.className] == nil then
|
||||
local fullName = ("%s.%s"):format(instance.className, self.name)
|
||||
return false, Error.new(Error.Kind.PropertyNotReadable, fullName)
|
||||
end
|
||||
|
||||
local interface = customProperties[self.className][self.name]
|
||||
|
||||
return interface.read(instance, self.name)
|
||||
@@ -84,11 +79,6 @@ function PropertyDescriptor:write(instance, value)
|
||||
end
|
||||
|
||||
if self.scriptability == "Custom" then
|
||||
if customProperties[self.className] == nil then
|
||||
local fullName = ("%s.%s"):format(instance.className, self.name)
|
||||
return false, Error.new(Error.Kind.PropertyNotWritable, fullName)
|
||||
end
|
||||
|
||||
local interface = customProperties[self.className][self.name]
|
||||
|
||||
return interface.write(instance, self.name, value)
|
||||
|
||||
@@ -1,79 +1,4 @@
|
||||
{
|
||||
"Attributes": {
|
||||
"value": {
|
||||
"Attributes": {
|
||||
"TestBool": {
|
||||
"Bool": true
|
||||
},
|
||||
"TestBrickColor": {
|
||||
"BrickColor": 24
|
||||
},
|
||||
"TestColor3": {
|
||||
"Color3": [
|
||||
1.0,
|
||||
0.5,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
"TestEnumItem": {
|
||||
"EnumItem": {
|
||||
"type": "Material",
|
||||
"value": 256
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"value": {
|
||||
"Axes": [
|
||||
@@ -176,23 +101,9 @@
|
||||
},
|
||||
"ty": "ColorSequence"
|
||||
},
|
||||
"ContentId": {
|
||||
"Content": {
|
||||
"value": {
|
||||
"ContentId": "rbxassetid://12345"
|
||||
},
|
||||
"ty": "ContentId"
|
||||
},
|
||||
"Content_None": {
|
||||
"value": {
|
||||
"Content": "None"
|
||||
},
|
||||
"ty": "Content"
|
||||
},
|
||||
"Content_Uri": {
|
||||
"value": {
|
||||
"Content": {
|
||||
"Uri": "rbxasset://abc/123.rojo"
|
||||
}
|
||||
"Content": "rbxassetid://12345"
|
||||
},
|
||||
"ty": "Content"
|
||||
},
|
||||
@@ -202,15 +113,6 @@
|
||||
},
|
||||
"ty": "Enum"
|
||||
},
|
||||
"EnumItem": {
|
||||
"value": {
|
||||
"EnumItem": {
|
||||
"type": "Material",
|
||||
"value": 256
|
||||
}
|
||||
},
|
||||
"ty": "EnumItem"
|
||||
},
|
||||
"Faces": {
|
||||
"value": {
|
||||
"Faces": [
|
||||
@@ -236,17 +138,6 @@
|
||||
},
|
||||
"ty": "Float64"
|
||||
},
|
||||
"Font": {
|
||||
"value": {
|
||||
"Font": {
|
||||
"family": "rbxasset://fonts/families/SourceSansPro.json",
|
||||
"weight": "Regular",
|
||||
"style": "Normal",
|
||||
"cachedFaceId": null
|
||||
}
|
||||
},
|
||||
"ty": "Font"
|
||||
},
|
||||
"Int32": {
|
||||
"value": {
|
||||
"Int32": 6014
|
||||
@@ -259,118 +150,6 @@
|
||||
},
|
||||
"ty": "Int64"
|
||||
},
|
||||
"MaterialColors": {
|
||||
"value": {
|
||||
"MaterialColors": {
|
||||
"Grass": [
|
||||
106,
|
||||
127,
|
||||
63
|
||||
],
|
||||
"Slate": [
|
||||
63,
|
||||
127,
|
||||
107
|
||||
],
|
||||
"Concrete": [
|
||||
127,
|
||||
102,
|
||||
63
|
||||
],
|
||||
"Brick": [
|
||||
138,
|
||||
86,
|
||||
62
|
||||
],
|
||||
"Sand": [
|
||||
143,
|
||||
126,
|
||||
95
|
||||
],
|
||||
"WoodPlanks": [
|
||||
139,
|
||||
109,
|
||||
79
|
||||
],
|
||||
"Rock": [
|
||||
102,
|
||||
108,
|
||||
111
|
||||
],
|
||||
"Glacier": [
|
||||
101,
|
||||
176,
|
||||
234
|
||||
],
|
||||
"Snow": [
|
||||
195,
|
||||
199,
|
||||
218
|
||||
],
|
||||
"Sandstone": [
|
||||
137,
|
||||
90,
|
||||
71
|
||||
],
|
||||
"Mud": [
|
||||
58,
|
||||
46,
|
||||
36
|
||||
],
|
||||
"Basalt": [
|
||||
30,
|
||||
30,
|
||||
37
|
||||
],
|
||||
"Ground": [
|
||||
102,
|
||||
92,
|
||||
59
|
||||
],
|
||||
"CrackedLava": [
|
||||
232,
|
||||
156,
|
||||
74
|
||||
],
|
||||
"Asphalt": [
|
||||
115,
|
||||
123,
|
||||
107
|
||||
],
|
||||
"Cobblestone": [
|
||||
132,
|
||||
123,
|
||||
90
|
||||
],
|
||||
"Ice": [
|
||||
129,
|
||||
194,
|
||||
224
|
||||
],
|
||||
"LeafyGrass": [
|
||||
115,
|
||||
132,
|
||||
74
|
||||
],
|
||||
"Salt": [
|
||||
198,
|
||||
189,
|
||||
181
|
||||
],
|
||||
"Limestone": [
|
||||
206,
|
||||
173,
|
||||
148
|
||||
],
|
||||
"Pavement": [
|
||||
148,
|
||||
148,
|
||||
140
|
||||
]
|
||||
}
|
||||
},
|
||||
"ty": "MaterialColors"
|
||||
},
|
||||
"NumberRange": {
|
||||
"value": {
|
||||
"NumberRange": [
|
||||
@@ -399,41 +178,6 @@
|
||||
},
|
||||
"ty": "NumberSequence"
|
||||
},
|
||||
"OptionalCFrame-None": {
|
||||
"value": {
|
||||
"OptionalCFrame": null
|
||||
},
|
||||
"ty": "OptionalCFrame"
|
||||
},
|
||||
"OptionalCFrame-Some": {
|
||||
"value": {
|
||||
"OptionalCFrame": {
|
||||
"position": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"orientation": [
|
||||
[
|
||||
1.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
1.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"ty": "OptionalCFrame"
|
||||
},
|
||||
"PhysicalProperties-Custom": {
|
||||
"value": {
|
||||
"PhysicalProperties": {
|
||||
@@ -441,8 +185,7 @@
|
||||
"friction": 1.0,
|
||||
"elasticity": 0.0,
|
||||
"frictionWeight": 50.0,
|
||||
"elasticityWeight": 25.0,
|
||||
"acousticAbsorption": 0.15625
|
||||
"elasticityWeight": 25.0
|
||||
}
|
||||
},
|
||||
"ty": "PhysicalProperties"
|
||||
|
||||
@@ -136,4 +136,4 @@ end
|
||||
return {
|
||||
decode = decodeBase64,
|
||||
encode = encodeBase64,
|
||||
}
|
||||
}
|
||||
29
plugin/rbx_dom_lua/base64.spec.lua
Normal file
@@ -0,0 +1,29 @@
|
||||
return function()
|
||||
local base64 = require(script.Parent.base64)
|
||||
|
||||
it("should encode and decode", function()
|
||||
local function try(str, expected)
|
||||
local encoded = base64.encode(str)
|
||||
expect(encoded).to.equal(expected)
|
||||
expect(base64.decode(encoded)).to.equal(str)
|
||||
end
|
||||
|
||||
try("Man", "TWFu")
|
||||
try("Ma", "TWE=")
|
||||
try("M", "TQ==")
|
||||
try("ManM", "TWFuTQ==")
|
||||
try(
|
||||
[[Man is distinguished, not only by his reason, but by this ]]..
|
||||
[[singular passion from other animals, which is a lust of the ]]..
|
||||
[[mind, that by a perseverance of delight in the continued and ]]..
|
||||
[[indefatigable generation of knowledge, exceeds the short ]]..
|
||||
[[vehemence of any carnal pleasure.]],
|
||||
[[TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sI]]..
|
||||
[[GJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYW]]..
|
||||
[[xzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJ]]..
|
||||
[[zZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRl]]..
|
||||
[[ZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZ]]..
|
||||
[[SBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=]]
|
||||
)
|
||||
end)
|
||||
end
|
||||
@@ -1,99 +1,10 @@
|
||||
local CollectionService = game:GetService("CollectionService")
|
||||
local ScriptEditorService = game:GetService("ScriptEditorService")
|
||||
|
||||
local Error = require(script.Parent.Error)
|
||||
|
||||
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
|
||||
local TERRAIN_MATERIAL_COLORS = {
|
||||
Enum.Material.Grass,
|
||||
Enum.Material.Slate,
|
||||
Enum.Material.Concrete,
|
||||
Enum.Material.Brick,
|
||||
Enum.Material.Sand,
|
||||
Enum.Material.WoodPlanks,
|
||||
Enum.Material.Rock,
|
||||
Enum.Material.Glacier,
|
||||
Enum.Material.Snow,
|
||||
Enum.Material.Sandstone,
|
||||
Enum.Material.Mud,
|
||||
Enum.Material.Basalt,
|
||||
Enum.Material.Ground,
|
||||
Enum.Material.CrackedLava,
|
||||
Enum.Material.Asphalt,
|
||||
Enum.Material.Cobblestone,
|
||||
Enum.Material.Ice,
|
||||
Enum.Material.LeafyGrass,
|
||||
Enum.Material.Salt,
|
||||
Enum.Material.Limestone,
|
||||
Enum.Material.Pavement,
|
||||
}
|
||||
|
||||
local function isAttributeNameValid(attributeName)
|
||||
-- For SetAttribute to succeed, the attribute name must be less than or
|
||||
-- equal to 100 characters...
|
||||
return #attributeName <= 100
|
||||
-- ...and must only contain alphanumeric characters, periods, hyphens,
|
||||
-- underscores, or forward slashes.
|
||||
and attributeName:match("[^%w%.%-_/]") == nil
|
||||
end
|
||||
|
||||
local function isAttributeNameReserved(attributeName)
|
||||
-- For SetAttribute to succeed, attribute names must not use the RBX
|
||||
-- prefix, which is reserved by Roblox.
|
||||
return attributeName:sub(1, 3) == "RBX"
|
||||
end
|
||||
|
||||
-- Defines how to read and write properties that aren't directly scriptable.
|
||||
--
|
||||
-- The reflection database refers to these as having scriptability = "Custom"
|
||||
return {
|
||||
Instance = {
|
||||
Attributes = {
|
||||
read = function(instance)
|
||||
return true, instance:GetAttributes()
|
||||
end,
|
||||
write = function(instance, _, value)
|
||||
if typeof(value) ~= "table" then
|
||||
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||
end
|
||||
|
||||
local existing = instance:GetAttributes()
|
||||
local didAllWritesSucceed = true
|
||||
|
||||
for attributeName, attributeValue in pairs(value) do
|
||||
if isAttributeNameReserved(attributeName) then
|
||||
-- If the attribute name is reserved, then we don't
|
||||
-- really care about reporting any failures about
|
||||
-- it.
|
||||
continue
|
||||
end
|
||||
|
||||
if not isAttributeNameValid(attributeName) then
|
||||
didAllWritesSucceed = false
|
||||
continue
|
||||
end
|
||||
|
||||
instance:SetAttribute(attributeName, attributeValue)
|
||||
end
|
||||
|
||||
for existingAttributeName in pairs(existing) do
|
||||
if isAttributeNameReserved(existingAttributeName) then
|
||||
continue
|
||||
end
|
||||
|
||||
if not isAttributeNameValid(existingAttributeName) then
|
||||
didAllWritesSucceed = false
|
||||
continue
|
||||
end
|
||||
|
||||
if value[existingAttributeName] == nil then
|
||||
instance:SetAttribute(existingAttributeName, nil)
|
||||
end
|
||||
end
|
||||
|
||||
return didAllWritesSucceed
|
||||
end,
|
||||
},
|
||||
Tags = {
|
||||
read = function(instance)
|
||||
return true, CollectionService:GetTags(instance)
|
||||
@@ -121,91 +32,13 @@ return {
|
||||
},
|
||||
LocalizationTable = {
|
||||
Contents = {
|
||||
read = function(instance, _)
|
||||
read = function(instance, key)
|
||||
return true, instance:GetContents()
|
||||
end,
|
||||
write = function(instance, _, value)
|
||||
write = function(instance, key, value)
|
||||
instance:SetContents(value)
|
||||
return true
|
||||
end,
|
||||
},
|
||||
},
|
||||
Model = {
|
||||
Scale = {
|
||||
read = function(instance, _, _)
|
||||
return true, instance:GetScale()
|
||||
end,
|
||||
write = function(instance, _, value)
|
||||
return true, instance:ScaleTo(value)
|
||||
end,
|
||||
},
|
||||
WorldPivotData = {
|
||||
read = function(instance)
|
||||
return true, instance.WorldPivot
|
||||
end,
|
||||
write = function(instance, _, value)
|
||||
if value == nil then
|
||||
return true, nil
|
||||
else
|
||||
instance.WorldPivot = value
|
||||
return true
|
||||
end
|
||||
end,
|
||||
},
|
||||
},
|
||||
Terrain = {
|
||||
MaterialColors = {
|
||||
read = function(instance: Terrain)
|
||||
-- There's no way to get a list of every color, so we have to
|
||||
-- make one.
|
||||
local colors = {}
|
||||
for _, material in TERRAIN_MATERIAL_COLORS do
|
||||
colors[material] = instance:GetMaterialColor(material)
|
||||
end
|
||||
|
||||
return true, colors
|
||||
end,
|
||||
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
|
||||
if typeof(value) ~= "table" then
|
||||
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||
end
|
||||
|
||||
for material, color in value do
|
||||
instance:SetMaterialColor(material, color)
|
||||
end
|
||||
|
||||
return true
|
||||
end,
|
||||
},
|
||||
},
|
||||
Script = {
|
||||
Source = {
|
||||
read = function(instance: Script)
|
||||
return true, ScriptEditorService:GetEditorSource(instance)
|
||||
end,
|
||||
write = function(instance: Script, _, value: string)
|
||||
task.spawn(function()
|
||||
ScriptEditorService:UpdateSourceAsync(instance, function()
|
||||
return value
|
||||
end)
|
||||
end)
|
||||
return true
|
||||
end,
|
||||
},
|
||||
},
|
||||
ModuleScript = {
|
||||
Source = {
|
||||
read = function(instance: ModuleScript)
|
||||
return true, ScriptEditorService:GetEditorSource(instance)
|
||||
end,
|
||||
write = function(instance: ModuleScript, _, value: string)
|
||||
task.spawn(function()
|
||||
ScriptEditorService:UpdateSourceAsync(instance, function()
|
||||
return value
|
||||
end)
|
||||
end)
|
||||
return true
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -24,8 +24,7 @@ local function findCanonicalPropertyDescriptor(className, propertyName)
|
||||
return PropertyDescriptor.fromRaw(
|
||||
currentClass.Properties[aliasData.AliasFor],
|
||||
currentClassName,
|
||||
aliasData.AliasFor
|
||||
)
|
||||
aliasData.AliasFor)
|
||||
end
|
||||
|
||||
return nil
|
||||
|
||||
7
plugin/rbx_dom_lua/init.spec.lua
Normal file
@@ -0,0 +1,7 @@
|
||||
return function()
|
||||
local RbxDom = require(script.Parent)
|
||||
|
||||
it("should load", function()
|
||||
expect(RbxDom).to.be.ok()
|
||||
end)
|
||||
end
|
||||
@@ -1,11 +1,19 @@
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local TestEZ = require(ReplicatedStorage.Packages:WaitForChild("TestEZ", 10))
|
||||
local TestEZ = require(ReplicatedStorage.TestEZ)
|
||||
|
||||
local Rojo = ReplicatedStorage.Rojo
|
||||
|
||||
local Settings = require(Rojo.Plugin.Settings)
|
||||
Settings:set("logLevel", "Trace")
|
||||
Settings:set("typecheckingEnabled", true)
|
||||
local DevSettings = require(Rojo.Plugin.DevSettings)
|
||||
|
||||
local setDevSettings = not DevSettings:hasChangedValues()
|
||||
|
||||
if setDevSettings then
|
||||
DevSettings:createTestSettings()
|
||||
end
|
||||
|
||||
require(Rojo.Plugin.runTests)(TestEZ)
|
||||
|
||||
if setDevSettings then
|
||||
DevSettings:resetValues()
|
||||
end
|
||||