mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-23 22:25:26 +00:00
Compare commits
74 Commits
v0.6.0-alp
...
memofs-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1214fc8b0d | ||
|
|
5a5b1268d3 | ||
|
|
6a1fffd1ce | ||
|
|
571ef3060a | ||
|
|
3cf82e112f | ||
|
|
9b459c20d6 | ||
|
|
5c85cd27c3 | ||
|
|
4bf73c7a8a | ||
|
|
62e51b7535 | ||
|
|
729a7f0053 | ||
|
|
03c297190d | ||
|
|
9c790eddd7 | ||
|
|
8ebe7e332b | ||
|
|
f43777e37e | ||
|
|
691a8fcdeb | ||
|
|
69c0e8d70e | ||
|
|
330c92c9a8 | ||
|
|
cf0ff60d31 | ||
|
|
9e9cf5dd1f | ||
|
|
5768d8e4a4 | ||
|
|
3b433e53be | ||
|
|
28ddf40344 | ||
|
|
c1286db9c1 | ||
|
|
f13940262e | ||
|
|
9f0a6101b8 | ||
|
|
0b0fe01a7c | ||
|
|
85e098d5c8 | ||
|
|
e8d1faf4e2 | ||
|
|
2a46df1110 | ||
|
|
1601e6d26e | ||
|
|
0e4f6dea2b | ||
|
|
a2356773dc | ||
|
|
4a4da4737d | ||
|
|
2cefd1bf2e | ||
|
|
c5ce15fe34 | ||
|
|
76dea568c9 | ||
|
|
8e81140eff | ||
|
|
d58e1f0792 | ||
|
|
830c242751 | ||
|
|
91d45afd0f | ||
|
|
102c77b23e | ||
|
|
aa4039a2e7 | ||
|
|
c065ded440 | ||
|
|
f69096dadb | ||
|
|
363f95ba14 | ||
|
|
dcc15e8911 | ||
|
|
bd13047b58 | ||
|
|
1a83789c01 | ||
|
|
1cbe272e19 | ||
|
|
6de74b41b3 | ||
|
|
b0fc9ee507 | ||
|
|
a95ffe1d31 | ||
|
|
4119a510f5 | ||
|
|
cfa7f03815 | ||
|
|
9b4c89820d | ||
|
|
fe874720aa | ||
|
|
f7c0f33eb5 | ||
|
|
c1bf9d9dfc | ||
|
|
255bf439d3 | ||
|
|
2a31937b81 | ||
|
|
eb8964e1d1 | ||
|
|
fe0ca280a1 | ||
|
|
e8e3b7b985 | ||
|
|
c437507442 | ||
|
|
ca151b434e | ||
|
|
10ba74c21e | ||
|
|
6191b6371d | ||
|
|
5be4175ac3 | ||
|
|
f61f3671a6 | ||
|
|
477e0ada32 | ||
|
|
a884f693ae | ||
|
|
3107b1b21b | ||
|
|
04529de7b3 | ||
|
|
199a39208c |
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -1,6 +1,9 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on: [push]
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: ["*"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -13,6 +16,8 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Rust toolchain
|
||||||
run: rustup default ${{ matrix.rust_version }}
|
run: rustup default ${{ matrix.rust_version }}
|
||||||
|
|||||||
5
.gitmodules
vendored
5
.gitmodules
vendored
@@ -9,7 +9,4 @@
|
|||||||
url = https://github.com/LPGhatguy/roblox-lua-promise.git
|
url = https://github.com/LPGhatguy/roblox-lua-promise.git
|
||||||
[submodule "plugin/modules/t"]
|
[submodule "plugin/modules/t"]
|
||||||
path = plugin/modules/t
|
path = plugin/modules/t
|
||||||
url = https://github.com/osyrisrblx/t.git
|
url = https://github.com/osyrisrblx/t.git
|
||||||
[submodule "plugin/modules/rbx-dom"]
|
|
||||||
path = plugin/modules/rbx-dom
|
|
||||||
url = http://github.com/rojo-rbx/rbx-dom
|
|
||||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,6 +1,32 @@
|
|||||||
# Rojo Changelog
|
# Rojo Changelog
|
||||||
|
|
||||||
## Unreleased Changes for 0.6.x
|
## Unreleased Changes
|
||||||
|
|
||||||
|
## [6.0.0 Release Candidate 1](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.0-rc.1) (March 29, 2020)
|
||||||
|
This release jumped from 0.6.0 to 6.0.0. Rojo has been in use in production for many users for quite a long times, and so 6.0 is a more accurate reflection of Rojo's version than a pre-1.0 version.
|
||||||
|
|
||||||
|
* Added basic settings panel to plugin, with two settings:
|
||||||
|
* "Open Scripts Externally": When enabled, opening a script in Studio will instead open it in your default text editor.
|
||||||
|
* "Two-Way Sync": When enabled, Rojo will attempt to save changes to your place back to the filesystem. **Very early feature, very broken, beware!**
|
||||||
|
* Added `--color` option to force-enable or force-disable color in Rojo's output.
|
||||||
|
* Added support for turning `.json` files into `ModuleScript` instances ([#308](https://github.com/rojo-rbx/rojo/pull/308))
|
||||||
|
* Added `rojo plugin install` and `rojo plugin uninstall` to allow Rojo to manage its Roblox Studio plugin. ([#304](https://github.com/rojo-rbx/rojo/pull/304))
|
||||||
|
* Class names no longer need to be specified for Roblox services in Rojo projects. ([#210](https://github.com/rojo-rbx/rojo/pull/210))
|
||||||
|
* The server half of **experimental** two-way sync is now enabled by default.
|
||||||
|
* Increased default logging verbosity in commands like `rojo build`.
|
||||||
|
* Rojo now requires a project file again, just like 0.5.4.
|
||||||
|
|
||||||
|
## [0.6.0 Alpha 3](https://github.com/rojo-rbx/rojo/releases/tag/v0.6.0-alpha.3) (March 13, 2020)
|
||||||
|
* Added `--watch` argument to `rojo build`. ([#284](https://github.com/rojo-rbx/rojo/pull/284))
|
||||||
|
* Added dark theme support to plugin. ([#241](https://github.com/rojo-rbx/rojo/issues/241))
|
||||||
|
* Added a revamped `rojo init` command, which will now create more complete projects.
|
||||||
|
* Added the `rojo doc` command, which opens Rojo's documentation in your browser.
|
||||||
|
* Fixed many crashes from malformed projects and filesystem edge cases in `rojo serve`.
|
||||||
|
* Simplified filesystem access code dramatically.
|
||||||
|
* Improved error reporting and logging across the board.
|
||||||
|
* Log messages have a less noisy prefix.
|
||||||
|
* Any thread panicking now causes Rojo to abort instead of existing as a zombie.
|
||||||
|
* Errors now have a list of causes, helping make many errors more clear.
|
||||||
|
|
||||||
## [0.6.0 Alpha 2](https://github.com/rojo-rbx/rojo/releases/tag/v0.6.0-alpha.2) (March 6, 2020)
|
## [0.6.0 Alpha 2](https://github.com/rojo-rbx/rojo/releases/tag/v0.6.0-alpha.2) (March 6, 2020)
|
||||||
* Fixed `rojo upload` command always uploading models.
|
* Fixed `rojo upload` command always uploading models.
|
||||||
|
|||||||
@@ -33,15 +33,13 @@ Please file issues and we'll try to help figure out what the best way forward is
|
|||||||
## Pushing a Rojo Release
|
## Pushing a Rojo Release
|
||||||
The Rojo release process is pretty manual right now. If you need to do it, here's how:
|
The Rojo release process is pretty manual right now. If you need to do it, here's how:
|
||||||
|
|
||||||
1. Bump server version in [`server/Cargo.toml`](server/Cargo.toml)
|
1. Bump server version in [`Cargo.toml`](Cargo.toml)
|
||||||
2. Bump plugin version in [`plugin/src/Config.lua`](plugin/src/Config.lua)
|
2. Bump plugin version in [`plugin/src/Config.lua`](plugin/src/Config.lua)
|
||||||
3. Run `cargo test` to update `Cargo.lock` and double-check tests
|
3. Run `cargo test` to update `Cargo.lock` and double-check tests
|
||||||
4. Update [`CHANGELOG.md`](CHANGELOG.md)
|
4. Update [`CHANGELOG.md`](CHANGELOG.md)
|
||||||
5. Commit!
|
5. Commit!
|
||||||
* `git add . && git commit -m "Release vX.Y.Z"`
|
* `git add . && git commit -m "Release vX.Y.Z"`
|
||||||
6. Tag the commit with the version from `Cargo.toml` prepended with a v, like `v0.4.13`
|
6. Tag the commit with the version from `Cargo.toml` prepended with a v, like `v0.4.13`
|
||||||
7. Build Windows release build of CLI
|
|
||||||
* `cargo build --release`
|
|
||||||
7. Publish the CLI
|
7. Publish the CLI
|
||||||
* `cargo publish`
|
* `cargo publish`
|
||||||
8. Build and upload the plugin
|
8. Build and upload the plugin
|
||||||
@@ -52,4 +50,5 @@ The Rojo release process is pretty manual right now. If you need to do it, here'
|
|||||||
10. Copy GitHub release content from previous release
|
10. Copy GitHub release content from previous release
|
||||||
* Update the leading text with a summary about the release
|
* Update the leading text with a summary about the release
|
||||||
* Paste the changelog notes (as-is!) from [`CHANGELOG.md`](CHANGELOG.md)
|
* Paste the changelog notes (as-is!) from [`CHANGELOG.md`](CHANGELOG.md)
|
||||||
* Write a small summary of each major feature
|
* Write a small summary of each major feature
|
||||||
|
* Attach release artifacts from GitHub Actions for each platform
|
||||||
2261
Cargo.lock
generated
2261
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
36
Cargo.toml
36
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "0.6.0-alpha.2"
|
version = "6.0.0-rc.1"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
description = "Enables professional-grade development tools for Roblox developers"
|
description = "Enables professional-grade development tools for Roblox developers"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
@@ -11,19 +11,21 @@ readme = "README.md"
|
|||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
exclude = [
|
exclude = [
|
||||||
"/plugin/**",
|
|
||||||
"/test-projects/**",
|
"/test-projects/**",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|
||||||
# Turn on support for specifying glob ignore path rules in the project format.
|
# Turn on support for specifying glob ignore path rules in the project format.
|
||||||
unstable_glob_ignore_paths = []
|
unstable_glob_ignore_paths = []
|
||||||
|
|
||||||
# Turn on the server half of Rojo's unstable two-way sync feature.
|
|
||||||
unstable_two_way_sync = []
|
|
||||||
|
|
||||||
# Enable this feature to live-reload assets from the web UI.
|
# Enable this feature to live-reload assets from the web UI.
|
||||||
dev_live_assets = []
|
dev_live_assets = []
|
||||||
|
|
||||||
@@ -32,14 +34,14 @@ members = [
|
|||||||
"rojo-test",
|
"rojo-test",
|
||||||
"rojo-insta-ext",
|
"rojo-insta-ext",
|
||||||
"clibrojo",
|
"clibrojo",
|
||||||
"vfs",
|
"memofs",
|
||||||
]
|
]
|
||||||
|
|
||||||
default-members = [
|
default-members = [
|
||||||
".",
|
".",
|
||||||
"rojo-test",
|
"rojo-test",
|
||||||
"rojo-insta-ext",
|
"rojo-insta-ext",
|
||||||
"vfs",
|
"memofs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
@@ -55,9 +57,15 @@ name = "build"
|
|||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
memofs = { version = "0.1.2", path = "memofs" }
|
||||||
|
|
||||||
|
anyhow = "1.0.27"
|
||||||
|
backtrace = "0.3"
|
||||||
|
bincode = "1.2.1"
|
||||||
crossbeam-channel = "0.4.0"
|
crossbeam-channel = "0.4.0"
|
||||||
csv = "1.1.1"
|
csv = "1.1.1"
|
||||||
env_logger = "0.7.1"
|
env_logger = "0.7.1"
|
||||||
|
fs-err = "2.2.0"
|
||||||
futures = "0.1.29"
|
futures = "0.1.29"
|
||||||
globset = "0.4.4"
|
globset = "0.4.4"
|
||||||
humantime = "1.3.0"
|
humantime = "1.3.0"
|
||||||
@@ -67,6 +75,7 @@ lazy_static = "1.4.0"
|
|||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
maplit = "1.0.1"
|
maplit = "1.0.1"
|
||||||
notify = "4.0.14"
|
notify = "4.0.14"
|
||||||
|
opener = "0.4.1"
|
||||||
rbx_binary = "0.5.0"
|
rbx_binary = "0.5.0"
|
||||||
rbx_dom_weak = "1.10.1"
|
rbx_dom_weak = "1.10.1"
|
||||||
rbx_reflection = "3.3.408"
|
rbx_reflection = "3.3.408"
|
||||||
@@ -75,16 +84,26 @@ regex = "1.3.1"
|
|||||||
reqwest = "0.9.20"
|
reqwest = "0.9.20"
|
||||||
ritz = "0.1.0"
|
ritz = "0.1.0"
|
||||||
rlua = "0.17.0"
|
rlua = "0.17.0"
|
||||||
|
roblox_install = "0.2.2"
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
snafu = "0.6.0"
|
|
||||||
structopt = "0.3.5"
|
structopt = "0.3.5"
|
||||||
termcolor = "1.0.5"
|
termcolor = "1.0.5"
|
||||||
|
thiserror = "1.0.11"
|
||||||
|
tokio = "0.1.22"
|
||||||
uuid = { version = "0.8.1", features = ["v4", "serde"] }
|
uuid = { version = "0.8.1", features = ["v4", "serde"] }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.6.2"
|
winreg = "0.6.2"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
memofs = { version = "0.1.0", path = "memofs" }
|
||||||
|
|
||||||
|
anyhow = "1.0.27"
|
||||||
|
bincode = "1.2.1"
|
||||||
|
fs-err = "2.3.0"
|
||||||
|
maplit = "1.0.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rojo-insta-ext = { path = "rojo-insta-ext" }
|
rojo-insta-ext = { path = "rojo-insta-ext" }
|
||||||
|
|
||||||
@@ -95,5 +114,4 @@ paste = "0.1"
|
|||||||
pretty_assertions = "0.6.1"
|
pretty_assertions = "0.6.1"
|
||||||
serde_yaml = "0.8.9"
|
serde_yaml = "0.8.9"
|
||||||
tempfile = "3.0"
|
tempfile = "3.0"
|
||||||
tokio = "0.1.22"
|
|
||||||
walkdir = "2.1"
|
walkdir = "2.1"
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
<a href="https://crates.io/crates/rojo">
|
<a href="https://crates.io/crates/rojo">
|
||||||
<img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" />
|
<img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://rojo.space/docs/0.5.x">
|
<a href="https://rojo.space/docs">
|
||||||
<img src="https://img.shields.io/badge/docs-0.5.x-brightgreen.svg" alt="Rojo 0.5.x Documentation" />
|
<img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,11 +34,10 @@ Rojo enables:
|
|||||||
* Streaming `rbxmx` and `rbxm` models into your game in real time
|
* Streaming `rbxmx` and `rbxm` models into your game in real time
|
||||||
* Packaging and deploying your project to Roblox.com from the command line
|
* Packaging and deploying your project to Roblox.com from the command line
|
||||||
|
|
||||||
Soon, Rojo will be able to:
|
In the future, Rojo will be able to:
|
||||||
|
|
||||||
* Automatically convert your existing game to work with Rojo
|
|
||||||
* Sync instances from Roblox Studio to the filesystem
|
* Sync instances from Roblox Studio to the filesystem
|
||||||
* Automatically manage your assets on Roblox.com, like images and sounds
|
* Automatically convert your existing game to work with Rojo
|
||||||
* Import custom instances like MoonScript code
|
* Import custom instances like MoonScript code
|
||||||
|
|
||||||
## [Documentation](https://rojo.space/docs)
|
## [Documentation](https://rojo.space/docs)
|
||||||
|
|||||||
11
assets/default-model-project/README.md
Normal file
11
assets/default-model-project/README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# {project_name}
|
||||||
|
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
To build this library or plugin, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rojo build -o "{project_name}.rbxmx"
|
||||||
|
```
|
||||||
|
|
||||||
|
For more help, check out [the Rojo documentation](https://rojo.space/docs).
|
||||||
6
assets/default-model-project/default.project.json
Normal file
6
assets/default-model-project/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "{project_name}",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
assets/default-model-project/gitignore.txt
Normal file
3
assets/default-model-project/gitignore.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Roblox Studio lock files
|
||||||
|
/*.rbxlx.lock
|
||||||
|
/*.rbxl.lock
|
||||||
5
assets/default-model-project/src-init.lua
Normal file
5
assets/default-model-project/src-init.lua
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
return {
|
||||||
|
hello = function()
|
||||||
|
print("Hello world, from {project_name}!")
|
||||||
|
end,
|
||||||
|
}
|
||||||
17
assets/default-place-project/README.md
Normal file
17
assets/default-place-project/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# {project_name}
|
||||||
|
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
To build the place from scratch, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rojo build -o "{project_name}.rbxlx"
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, open `{project_name}.rbxlx` in Roblox Studio and start the Rojo server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rojo serve
|
||||||
|
```
|
||||||
|
|
||||||
|
For more help, check out [the Rojo documentation](https://rojo.space/docs).
|
||||||
@@ -1,39 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "[placeholder]",
|
"name": "{project_name}",
|
||||||
"tree": {
|
"tree": {
|
||||||
"$className": "DataModel",
|
"$className": "DataModel",
|
||||||
"HttpService": {
|
|
||||||
"$className": "HttpService",
|
|
||||||
"$properties": {
|
|
||||||
"HttpEnabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Lighting": {
|
|
||||||
"$className": "Lighting",
|
|
||||||
"$properties": {
|
|
||||||
"Ambient": [
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"Brightness": 2,
|
|
||||||
"GlobalShadows": true,
|
|
||||||
"Outlines": false,
|
|
||||||
"Technology": "Voxel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ReplicatedStorage": {
|
"ReplicatedStorage": {
|
||||||
"$className": "ReplicatedStorage",
|
"$className": "ReplicatedStorage",
|
||||||
"Source": {
|
|
||||||
"$path": "src"
|
"Common": {
|
||||||
|
"$path": "src/shared"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SoundService": {
|
|
||||||
"$className": "SoundService",
|
"ServerScriptService": {
|
||||||
"$properties": {
|
"$className": "ServerScriptService",
|
||||||
"RespectFilteringEnabled": true
|
|
||||||
|
"Server": {
|
||||||
|
"$path": "src/server"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"StarterPlayer": {
|
||||||
|
"$className": "StarterPlayer",
|
||||||
|
|
||||||
|
"StarterPlayerScripts": {
|
||||||
|
"$className": "StarterPlayerScripts",
|
||||||
|
|
||||||
|
"Client": {
|
||||||
|
"$path": "src/client"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"Workspace": {
|
"Workspace": {
|
||||||
"$className": "Workspace",
|
"$className": "Workspace",
|
||||||
"$properties": {
|
"$properties": {
|
||||||
@@ -61,6 +58,32 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Lighting": {
|
||||||
|
"$className": "Lighting",
|
||||||
|
"$properties": {
|
||||||
|
"Ambient": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"Brightness": 2,
|
||||||
|
"GlobalShadows": true,
|
||||||
|
"Outlines": false,
|
||||||
|
"Technology": "Voxel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SoundService": {
|
||||||
|
"$className": "SoundService",
|
||||||
|
"$properties": {
|
||||||
|
"RespectFilteringEnabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HttpService": {
|
||||||
|
"$className": "HttpService",
|
||||||
|
"$properties": {
|
||||||
|
"HttpEnabled": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
assets/default-place-project/gitignore.txt
Normal file
6
assets/default-place-project/gitignore.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Project place file
|
||||||
|
/{project_name}.rbxlx
|
||||||
|
|
||||||
|
# Roblox Studio lock files
|
||||||
|
/*.rbxlx.lock
|
||||||
|
/*.rbxl.lock
|
||||||
74
build.rs
Normal file
74
build.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
use std::{
|
||||||
|
env, io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use fs_err as fs;
|
||||||
|
use fs_err::File;
|
||||||
|
use maplit::hashmap;
|
||||||
|
use memofs::VfsSnapshot;
|
||||||
|
|
||||||
|
fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||||
|
println!("cargo:rerun-if-changed={}", path.display());
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
let mut children = Vec::new();
|
||||||
|
|
||||||
|
for entry in fs::read_dir(path)? {
|
||||||
|
let entry = entry?;
|
||||||
|
|
||||||
|
let file_name = entry.file_name().to_str().unwrap().to_owned();
|
||||||
|
|
||||||
|
// We can skip any TestEZ test files since they aren't necessary for
|
||||||
|
// the plugin to run.
|
||||||
|
if file_name.ends_with(".spec.lua") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let child_snapshot = snapshot_from_fs_path(&entry.path())?;
|
||||||
|
children.push((file_name, child_snapshot));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(VfsSnapshot::dir(children))
|
||||||
|
} else {
|
||||||
|
let content = fs::read_to_string(path)?;
|
||||||
|
|
||||||
|
Ok(VfsSnapshot::file(content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), anyhow::Error> {
|
||||||
|
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||||
|
|
||||||
|
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
||||||
|
let plugin_root = PathBuf::from(root_dir).join("plugin");
|
||||||
|
|
||||||
|
let plugin_modules = plugin_root.join("modules");
|
||||||
|
|
||||||
|
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"))?
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
let out_path = Path::new(&out_dir).join("plugin.bincode");
|
||||||
|
let out_file = File::create(&out_path)?;
|
||||||
|
|
||||||
|
bincode::serialize_into(out_file, &snapshot)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
30
design.gv
30
design.gv
@@ -1,30 +0,0 @@
|
|||||||
digraph Rojo {
|
|
||||||
concentrate = true;
|
|
||||||
node [fontname = "sans-serif"];
|
|
||||||
|
|
||||||
plugin [label="Roblox Studio Plugin"]
|
|
||||||
session [label="Session"]
|
|
||||||
rbx_tree [label="Instance Tree"]
|
|
||||||
imfs [label="In-Memory Filesystem"]
|
|
||||||
fs_impl [label="Filesystem Implementation\n(stubbed in tests)"]
|
|
||||||
fs [label="Real Filesystem"]
|
|
||||||
snapshot_subsystem [label="Snapshot Subsystem\n(reconciler)"]
|
|
||||||
snapshot_generator [label="Snapshot Generator"]
|
|
||||||
user_middleware [label="User Middleware\n(MoonScript, etc.)"]
|
|
||||||
builtin_middleware [label="Built-in Middleware\n(.lua, .rbxm, etc.)"]
|
|
||||||
api [label="Web API"]
|
|
||||||
file_watcher [label="File Watcher"]
|
|
||||||
|
|
||||||
session -> imfs
|
|
||||||
session -> rbx_tree
|
|
||||||
session -> snapshot_subsystem
|
|
||||||
session -> snapshot_generator
|
|
||||||
session -> file_watcher [dir="both"]
|
|
||||||
file_watcher -> imfs
|
|
||||||
snapshot_generator -> user_middleware
|
|
||||||
snapshot_generator -> builtin_middleware
|
|
||||||
plugin -> api [style="dotted"; dir="both"; minlen=2]
|
|
||||||
api -> session
|
|
||||||
imfs -> fs_impl
|
|
||||||
fs_impl -> fs
|
|
||||||
}
|
|
||||||
9
memofs/CHANGELOG.md
Normal file
9
memofs/CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# memofs Changelog
|
||||||
|
|
||||||
|
## Unreleased Changes
|
||||||
|
|
||||||
|
## 0.1.1 (2020-03-18)
|
||||||
|
* Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate.
|
||||||
|
|
||||||
|
## 0.1.0 (2020-03-10)
|
||||||
|
* Initial release
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "vfs"
|
name = "memofs"
|
||||||
version = "0.1.0"
|
description = "Virtual filesystem with configurable backends."
|
||||||
|
version = "0.1.2"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
homepage = "https://github.com/rojo-rbx/rojo"
|
homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
notify = "4.0.15"
|
|
||||||
crossbeam-channel = "0.4.0"
|
crossbeam-channel = "0.4.0"
|
||||||
|
fs-err = "2.3.0"
|
||||||
|
notify = "4.0.15"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
22
memofs/README.md
Normal file
22
memofs/README.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# memofs
|
||||||
|
[](https://crates.io/crates/memofs)
|
||||||
|
|
||||||
|
Implementation of a virtual filesystem with a configurable backend and file
|
||||||
|
watching.
|
||||||
|
|
||||||
|
memofs is currently an unstable minimum viable library. Its primary consumer is
|
||||||
|
[Rojo](https://github.com/rojo-rbx/rojo), a build system for Roblox.
|
||||||
|
|
||||||
|
### Current Features
|
||||||
|
* API similar to `std::fs`
|
||||||
|
* Configurable backends
|
||||||
|
* `StdBackend`, which uses `std::fs` and the `notify` crate
|
||||||
|
* `NoopBackend`, which always throws errors
|
||||||
|
* `InMemoryFs`, a simple in-memory filesystem useful for testing
|
||||||
|
|
||||||
|
### Future Features
|
||||||
|
* Hash-based hierarchical memoization keys (hence the name)
|
||||||
|
* Configurable caching (write-through, write-around, write-back)
|
||||||
|
|
||||||
|
## License
|
||||||
|
memofs is available under the terms of the MIT license. See [LICENSE.txt](LICENSE.txt) or <https://opensource.org/licenses/MIT> for more details.
|
||||||
7
memofs/README.tpl
Normal file
7
memofs/README.tpl
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# {{crate}}
|
||||||
|
[](https://crates.io/crates/memofs)
|
||||||
|
|
||||||
|
{{readme}}
|
||||||
|
|
||||||
|
## License
|
||||||
|
memofs is available under the terms of the MIT license. See [LICENSE.txt](LICENSE.txt) or <https://opensource.org/licenses/MIT> for more details.
|
||||||
@@ -1,32 +1,77 @@
|
|||||||
use std::collections::{BTreeSet, HashMap, VecDeque};
|
use std::collections::{BTreeSet, HashMap, VecDeque};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
|
|
||||||
use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent, VfsSnapshot};
|
use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent, VfsSnapshot};
|
||||||
|
|
||||||
/// `VfsBackend` that reads from an in-memory filesystem, intended for setting
|
/// In-memory filesystem that can be used as a VFS backend.
|
||||||
/// up testing scenarios quickly.
|
///
|
||||||
#[derive(Debug)]
|
/// Internally reference counted to enable giving a copy to
|
||||||
pub struct MemoryBackend {
|
/// [`Vfs`](struct.Vfs.html) and keeping the original to mutate the filesystem's
|
||||||
entries: HashMap<PathBuf, Entry>,
|
/// state with.
|
||||||
orphans: BTreeSet<PathBuf>,
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct InMemoryFs {
|
||||||
|
inner: Arc<Mutex<InMemoryFsInner>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MemoryBackend {
|
impl InMemoryFs {
|
||||||
|
/// Create a new empty `InMemoryFs`.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
entries: HashMap::new(),
|
inner: Arc::new(Mutex::new(InMemoryFsInner::new())),
|
||||||
orphans: BTreeSet::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load a [`VfsSnapshot`](enum.VfsSnapshot.html) into a subtree of the
|
||||||
|
/// in-memory filesystem.
|
||||||
|
///
|
||||||
|
/// This function will return an error if the operations required to apply
|
||||||
|
/// the snapshot result in errors, like trying to create a file inside a
|
||||||
|
/// file.
|
||||||
pub fn load_snapshot<P: Into<PathBuf>>(
|
pub fn load_snapshot<P: Into<PathBuf>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: P,
|
path: P,
|
||||||
snapshot: VfsSnapshot,
|
snapshot: VfsSnapshot,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let path = path.into();
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
inner.load_snapshot(path.into(), snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raises a filesystem change event.
|
||||||
|
///
|
||||||
|
/// If this `InMemoryFs` is being used as the backend of a
|
||||||
|
/// [`Vfs`](struct.Vfs.html), then any listeners be notified of this event.
|
||||||
|
pub fn raise_event(&mut self, event: VfsEvent) {
|
||||||
|
let inner = self.inner.lock().unwrap();
|
||||||
|
inner.event_sender.send(event).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct InMemoryFsInner {
|
||||||
|
entries: HashMap<PathBuf, Entry>,
|
||||||
|
orphans: BTreeSet<PathBuf>,
|
||||||
|
|
||||||
|
event_receiver: Receiver<VfsEvent>,
|
||||||
|
event_sender: Sender<VfsEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InMemoryFsInner {
|
||||||
|
fn new() -> Self {
|
||||||
|
let (event_sender, event_receiver) = crossbeam_channel::unbounded();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
entries: HashMap::new(),
|
||||||
|
orphans: BTreeSet::new(),
|
||||||
|
event_receiver,
|
||||||
|
event_sender,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_snapshot(&mut self, path: PathBuf, snapshot: VfsSnapshot) -> io::Result<()> {
|
||||||
if let Some(parent_path) = path.parent() {
|
if let Some(parent_path) = path.parent() {
|
||||||
if let Some(parent_entry) = self.entries.get_mut(parent_path) {
|
if let Some(parent_entry) = self.entries.get_mut(parent_path) {
|
||||||
if let Entry::Dir { children } = parent_entry {
|
if let Entry::Dir { children } = parent_entry {
|
||||||
@@ -84,9 +129,11 @@ enum Entry {
|
|||||||
Dir { children: BTreeSet<PathBuf> },
|
Dir { children: BTreeSet<PathBuf> },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VfsBackend for MemoryBackend {
|
impl VfsBackend for InMemoryFs {
|
||||||
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
||||||
match self.entries.get(path) {
|
let inner = self.inner.lock().unwrap();
|
||||||
|
|
||||||
|
match inner.entries.get(path) {
|
||||||
Some(Entry::File { contents }) => Ok(contents.clone()),
|
Some(Entry::File { contents }) => Ok(contents.clone()),
|
||||||
Some(Entry::Dir { .. }) => must_be_file(path),
|
Some(Entry::Dir { .. }) => must_be_file(path),
|
||||||
None => not_found(path),
|
None => not_found(path),
|
||||||
@@ -94,8 +141,10 @@ impl VfsBackend for MemoryBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
|
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
|
||||||
self.load_snapshot(
|
let mut inner = self.inner.lock().unwrap();
|
||||||
path,
|
|
||||||
|
inner.load_snapshot(
|
||||||
|
path.to_path_buf(),
|
||||||
VfsSnapshot::File {
|
VfsSnapshot::File {
|
||||||
contents: data.to_owned(),
|
contents: data.to_owned(),
|
||||||
},
|
},
|
||||||
@@ -103,7 +152,9 @@ impl VfsBackend for MemoryBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||||
match self.entries.get(path) {
|
let inner = self.inner.lock().unwrap();
|
||||||
|
|
||||||
|
match inner.entries.get(path) {
|
||||||
Some(Entry::Dir { children }) => {
|
Some(Entry::Dir { children }) => {
|
||||||
let iter = children
|
let iter = children
|
||||||
.clone()
|
.clone()
|
||||||
@@ -120,9 +171,11 @@ impl VfsBackend for MemoryBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
||||||
match self.entries.get(path) {
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
|
||||||
|
match inner.entries.get(path) {
|
||||||
Some(Entry::File { .. }) => {
|
Some(Entry::File { .. }) => {
|
||||||
self.remove(path.to_owned());
|
inner.remove(path.to_owned());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Some(Entry::Dir { .. }) => must_be_file(path),
|
Some(Entry::Dir { .. }) => must_be_file(path),
|
||||||
@@ -131,9 +184,11 @@ impl VfsBackend for MemoryBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
||||||
match self.entries.get(path) {
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
|
||||||
|
match inner.entries.get(path) {
|
||||||
Some(Entry::Dir { .. }) => {
|
Some(Entry::Dir { .. }) => {
|
||||||
self.remove(path.to_owned());
|
inner.remove(path.to_owned());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Some(Entry::File { .. }) => must_be_dir(path),
|
Some(Entry::File { .. }) => must_be_dir(path),
|
||||||
@@ -142,7 +197,9 @@ impl VfsBackend for MemoryBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn metadata(&mut self, path: &Path) -> io::Result<Metadata> {
|
fn metadata(&mut self, path: &Path) -> io::Result<Metadata> {
|
||||||
match self.entries.get(path) {
|
let inner = self.inner.lock().unwrap();
|
||||||
|
|
||||||
|
match inner.entries.get(path) {
|
||||||
Some(Entry::File { .. }) => Ok(Metadata { is_file: true }),
|
Some(Entry::File { .. }) => Ok(Metadata { is_file: true }),
|
||||||
Some(Entry::Dir { .. }) => Ok(Metadata { is_file: false }),
|
Some(Entry::Dir { .. }) => Ok(Metadata { is_file: false }),
|
||||||
None => not_found(path),
|
None => not_found(path),
|
||||||
@@ -150,7 +207,9 @@ impl VfsBackend for MemoryBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
crossbeam_channel::never()
|
let inner = self.inner.lock().unwrap();
|
||||||
|
|
||||||
|
inner.event_receiver.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
||||||
@@ -1,4 +1,23 @@
|
|||||||
mod memory_backend;
|
/*!
|
||||||
|
Implementation of a virtual filesystem with a configurable backend and file
|
||||||
|
watching.
|
||||||
|
|
||||||
|
memofs is currently an unstable minimum viable library. Its primary consumer is
|
||||||
|
[Rojo](https://github.com/rojo-rbx/rojo), a build system for Roblox.
|
||||||
|
|
||||||
|
## Current Features
|
||||||
|
* API similar to `std::fs`
|
||||||
|
* Configurable backends
|
||||||
|
* `StdBackend`, which uses `std::fs` and the `notify` crate
|
||||||
|
* `NoopBackend`, which always throws errors
|
||||||
|
* `InMemoryFs`, a simple in-memory filesystem useful for testing
|
||||||
|
|
||||||
|
## Future Features
|
||||||
|
* Hash-based hierarchical memoization keys (hence the name)
|
||||||
|
* Configurable caching (write-through, write-around, write-back)
|
||||||
|
*/
|
||||||
|
|
||||||
|
mod in_memory_fs;
|
||||||
mod noop_backend;
|
mod noop_backend;
|
||||||
mod snapshot;
|
mod snapshot;
|
||||||
mod std_backend;
|
mod std_backend;
|
||||||
@@ -7,7 +26,7 @@ use std::io;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, Mutex, MutexGuard};
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
|
|
||||||
pub use memory_backend::MemoryBackend;
|
pub use in_memory_fs::InMemoryFs;
|
||||||
pub use noop_backend::NoopBackend;
|
pub use noop_backend::NoopBackend;
|
||||||
pub use snapshot::VfsSnapshot;
|
pub use snapshot::VfsSnapshot;
|
||||||
pub use std_backend::StdBackend;
|
pub use std_backend::StdBackend;
|
||||||
@@ -18,9 +37,9 @@ mod sealed {
|
|||||||
/// Sealing trait for VfsBackend.
|
/// Sealing trait for VfsBackend.
|
||||||
pub trait Sealed {}
|
pub trait Sealed {}
|
||||||
|
|
||||||
impl Sealed for MemoryBackend {}
|
|
||||||
impl Sealed for NoopBackend {}
|
impl Sealed for NoopBackend {}
|
||||||
impl Sealed for StdBackend {}
|
impl Sealed for StdBackend {}
|
||||||
|
impl Sealed for InMemoryFs {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait that transforms `io::Result<T>` into `io::Result<Option<T>>`.
|
/// Trait that transforms `io::Result<T>` into `io::Result<Option<T>>`.
|
||||||
@@ -107,6 +126,8 @@ impl Metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents an event that a filesystem can raise that might need to be
|
||||||
|
/// handled.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum VfsEvent {
|
pub enum VfsEvent {
|
||||||
@@ -176,6 +197,10 @@ impl VfsInner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A virtual filesystem with a configurable backend.
|
/// A virtual filesystem with a configurable backend.
|
||||||
|
///
|
||||||
|
/// All operations on the Vfs take a lock on an internal backend. For performing
|
||||||
|
/// large batches of operations, it might be more performant to call `lock()`
|
||||||
|
/// and use [`VfsLock`](struct.VfsLock.html) instead.
|
||||||
pub struct Vfs {
|
pub struct Vfs {
|
||||||
inner: Mutex<VfsInner>,
|
inner: Mutex<VfsInner>,
|
||||||
}
|
}
|
||||||
@@ -197,6 +222,7 @@ impl Vfs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Manually lock the Vfs, useful for large batches of operations.
|
||||||
pub fn lock(&self) -> VfsLock<'_> {
|
pub fn lock(&self) -> VfsLock<'_> {
|
||||||
VfsLock {
|
VfsLock {
|
||||||
inner: self.inner.lock().unwrap(),
|
inner: self.inner.lock().unwrap(),
|
||||||
@@ -284,7 +310,9 @@ impl Vfs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A locked handle to a `Vfs`, created by `Vfs::lock`.
|
/// A locked handle to a [`Vfs`](struct.Vfs.html), created by `Vfs::lock`.
|
||||||
|
///
|
||||||
|
/// Implements roughly the same API as [`Vfs`](struct.Vfs.html).
|
||||||
pub struct VfsLock<'a> {
|
pub struct VfsLock<'a> {
|
||||||
inner: MutexGuard<'a, VfsInner>,
|
inner: MutexGuard<'a, VfsInner>,
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
/// A slice of a tree of files. Can be loaded into an
|
||||||
|
/// [`InMemoryFs`](struct.InMemoryFs.html).
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum VfsSnapshot {
|
pub enum VfsSnapshot {
|
||||||
File {
|
File {
|
||||||
@@ -26,4 +30,16 @@ impl VfsSnapshot {
|
|||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn empty_file() -> Self {
|
||||||
|
Self::File {
|
||||||
|
contents: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty_dir() -> Self {
|
||||||
|
Self::Dir {
|
||||||
|
children: BTreeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
use std::fs;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
@@ -55,19 +54,22 @@ impl StdBackend {
|
|||||||
|
|
||||||
impl VfsBackend for StdBackend {
|
impl VfsBackend for StdBackend {
|
||||||
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
||||||
fs::read(path)
|
fs_err::read(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
|
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
|
||||||
fs::write(path, data)
|
fs_err::write(path, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||||
let inner = fs::read_dir(path)?.map(|entry| {
|
let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect();
|
||||||
Ok(DirEntry {
|
let mut entries = entries?;
|
||||||
path: entry?.path(),
|
|
||||||
})
|
entries.sort_by_cached_key(|entry| entry.file_name());
|
||||||
});
|
|
||||||
|
let inner = entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| Ok(DirEntry { path: entry.path() }));
|
||||||
|
|
||||||
Ok(ReadDir {
|
Ok(ReadDir {
|
||||||
inner: Box::new(inner),
|
inner: Box::new(inner),
|
||||||
@@ -75,15 +77,15 @@ impl VfsBackend for StdBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
||||||
fs::remove_file(path)
|
fs_err::remove_file(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
||||||
fs::remove_dir_all(path)
|
fs_err::remove_dir_all(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn metadata(&mut self, path: &Path) -> io::Result<Metadata> {
|
fn metadata(&mut self, path: &Path) -> io::Result<Metadata> {
|
||||||
let inner = fs::metadata(path)?;
|
let inner = fs_err::metadata(path)?;
|
||||||
|
|
||||||
Ok(Metadata {
|
Ok(Metadata {
|
||||||
is_file: inner.is_file(),
|
is_file: inner.is_file(),
|
||||||
@@ -14,6 +14,9 @@
|
|||||||
"Fmt": {
|
"Fmt": {
|
||||||
"$path": "fmt"
|
"$path": "fmt"
|
||||||
},
|
},
|
||||||
|
"RbxDom": {
|
||||||
|
"$path": "rbx_dom_lua"
|
||||||
|
},
|
||||||
"Roact": {
|
"Roact": {
|
||||||
"$path": "modules/roact/src"
|
"$path": "modules/roact/src"
|
||||||
},
|
},
|
||||||
@@ -22,9 +25,6 @@
|
|||||||
},
|
},
|
||||||
"t": {
|
"t": {
|
||||||
"$path": "modules/t/lib"
|
"$path": "modules/t/lib"
|
||||||
},
|
|
||||||
"RbxDom": {
|
|
||||||
"$path": "modules/rbx-dom/rbx_dom_lua/src"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Submodule plugin/modules/rbx-dom deleted from 5bca08fec3
Submodule plugin/modules/roact updated: b1db3f82a2...f7d2f1ce1d
44
plugin/rbx_dom_lua/.luacheckrc
Normal file
44
plugin/rbx_dom_lua/.luacheckrc
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
stds.roblox = {
|
||||||
|
read_globals = {
|
||||||
|
game = {
|
||||||
|
other_fields = true,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Roblox globals
|
||||||
|
"script",
|
||||||
|
|
||||||
|
-- Extra functions
|
||||||
|
"tick", "warn",
|
||||||
|
"wait", "typeof",
|
||||||
|
|
||||||
|
-- Types
|
||||||
|
"CFrame",
|
||||||
|
"Color3",
|
||||||
|
"Enum",
|
||||||
|
"Instance",
|
||||||
|
"NumberRange",
|
||||||
|
"Rect",
|
||||||
|
"UDim", "UDim2",
|
||||||
|
"Vector2", "Vector3",
|
||||||
|
"Vector2int16", "Vector3int16",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stds.testez = {
|
||||||
|
read_globals = {
|
||||||
|
"describe",
|
||||||
|
"it", "itFOCUS", "itSKIP",
|
||||||
|
"FOCUS", "SKIP", "HACK_NO_XPCALL",
|
||||||
|
"expect",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ignore = {
|
||||||
|
"212", -- unused arguments
|
||||||
|
}
|
||||||
|
|
||||||
|
std = "lua51+roblox"
|
||||||
|
|
||||||
|
files["**/*.spec.lua"] = {
|
||||||
|
std = "+testez",
|
||||||
|
}
|
||||||
2
plugin/rbx_dom_lua/README.md
Normal file
2
plugin/rbx_dom_lua/README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# rbx\_dom\_lua
|
||||||
|
Roblox Lua implementation of rbx-dom mechanisms, intended to work with rbx\_dom\_weak and friends.
|
||||||
6
plugin/rbx_dom_lua/default.project.json
Normal file
6
plugin/rbx_dom_lua/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "rbx_dom_lua",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
242
plugin/rbx_dom_lua/src/EncodedValue.lua
Normal file
242
plugin/rbx_dom_lua/src/EncodedValue.lua
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
local base64 = require(script.Parent.base64)
|
||||||
|
|
||||||
|
local function identity(...)
|
||||||
|
return ...
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unpackDecoder(f)
|
||||||
|
return function(value)
|
||||||
|
return f(unpack(value))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function serializeFloat(value)
|
||||||
|
-- TODO: Figure out a better way to serialize infinity and NaN, neither of
|
||||||
|
-- which fit into JSON.
|
||||||
|
if value == math.huge or value == -math.huge then
|
||||||
|
return 999999999 * math.sign(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
local encoders
|
||||||
|
encoders = {
|
||||||
|
Bool = identity,
|
||||||
|
Content = identity,
|
||||||
|
Float32 = serializeFloat,
|
||||||
|
Float64 = serializeFloat,
|
||||||
|
Int32 = identity,
|
||||||
|
Int64 = identity,
|
||||||
|
String = identity,
|
||||||
|
|
||||||
|
BinaryString = base64.encode,
|
||||||
|
SharedString = base64.encode,
|
||||||
|
|
||||||
|
BrickColor = function(value)
|
||||||
|
return value.Number
|
||||||
|
end,
|
||||||
|
|
||||||
|
CFrame = function(value)
|
||||||
|
return {value:GetComponents()}
|
||||||
|
end,
|
||||||
|
Color3 = function(value)
|
||||||
|
return {value.r, value.g, value.b}
|
||||||
|
end,
|
||||||
|
NumberRange = function(value)
|
||||||
|
return {value.Min, value.Max}
|
||||||
|
end,
|
||||||
|
NumberSequence = function(value)
|
||||||
|
local keypoints = {}
|
||||||
|
|
||||||
|
for index, keypoint in ipairs(value.Keypoints) do
|
||||||
|
keypoints[index] = {
|
||||||
|
Time = keypoint.Time,
|
||||||
|
Value = keypoint.Value,
|
||||||
|
Envelope = keypoint.Envelope,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
Keypoints = keypoints,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
ColorSequence = function(value)
|
||||||
|
local keypoints = {}
|
||||||
|
|
||||||
|
for index, keypoint in ipairs(value.Keypoints) do
|
||||||
|
keypoints[index] = {
|
||||||
|
Time = keypoint.Time,
|
||||||
|
Color = encoders.Color3(keypoint.Value),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
Keypoints = keypoints,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
Rect = function(value)
|
||||||
|
return {
|
||||||
|
Min = {value.Min.X, value.Min.Y},
|
||||||
|
Max = {value.Max.X, value.Max.Y},
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
UDim = function(value)
|
||||||
|
return {value.Scale, value.Offset}
|
||||||
|
end,
|
||||||
|
UDim2 = function(value)
|
||||||
|
return {value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset}
|
||||||
|
end,
|
||||||
|
Vector2 = function(value)
|
||||||
|
return {
|
||||||
|
serializeFloat(value.X),
|
||||||
|
serializeFloat(value.Y),
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
Vector2int16 = function(value)
|
||||||
|
return {value.X, value.Y}
|
||||||
|
end,
|
||||||
|
Vector3 = function(value)
|
||||||
|
return {
|
||||||
|
serializeFloat(value.X),
|
||||||
|
serializeFloat(value.Y),
|
||||||
|
serializeFloat(value.Z),
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
Vector3int16 = function(value)
|
||||||
|
return {value.X, value.Y, value.Z}
|
||||||
|
end,
|
||||||
|
|
||||||
|
PhysicalProperties = function(value)
|
||||||
|
if value == nil then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
return {
|
||||||
|
Density = value.Density,
|
||||||
|
Friction = value.Friction,
|
||||||
|
Elasticity = value.Elasticity,
|
||||||
|
FrictionWeight = value.FrictionWeight,
|
||||||
|
ElasticityWeight = value.ElasticityWeight,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
Ref = function(value)
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local decoders = {
|
||||||
|
Bool = identity,
|
||||||
|
Content = identity,
|
||||||
|
Enum = identity,
|
||||||
|
Float32 = identity,
|
||||||
|
Float64 = identity,
|
||||||
|
Int32 = identity,
|
||||||
|
Int64 = identity,
|
||||||
|
String = identity,
|
||||||
|
|
||||||
|
BinaryString = base64.decode,
|
||||||
|
SharedString = base64.decode,
|
||||||
|
|
||||||
|
BrickColor = BrickColor.new,
|
||||||
|
|
||||||
|
CFrame = unpackDecoder(CFrame.new),
|
||||||
|
Color3 = unpackDecoder(Color3.new),
|
||||||
|
Color3uint8 = unpackDecoder(Color3.fromRGB),
|
||||||
|
NumberRange = unpackDecoder(NumberRange.new),
|
||||||
|
UDim = unpackDecoder(UDim.new),
|
||||||
|
UDim2 = unpackDecoder(UDim2.new),
|
||||||
|
Vector2 = unpackDecoder(Vector2.new),
|
||||||
|
Vector2int16 = unpackDecoder(Vector2int16.new),
|
||||||
|
Vector3 = unpackDecoder(Vector3.new),
|
||||||
|
Vector3int16 = unpackDecoder(Vector3int16.new),
|
||||||
|
|
||||||
|
Rect = function(value)
|
||||||
|
return Rect.new(value.Min[1], value.Min[2], value.Max[1], value.Max[2])
|
||||||
|
end,
|
||||||
|
|
||||||
|
NumberSequence = function(value)
|
||||||
|
local keypoints = {}
|
||||||
|
|
||||||
|
for index, keypoint in ipairs(value.Keypoints) do
|
||||||
|
keypoints[index] = NumberSequenceKeypoint.new(
|
||||||
|
keypoint.Time,
|
||||||
|
keypoint.Value,
|
||||||
|
keypoint.Envelope
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return NumberSequence.new(keypoints)
|
||||||
|
end,
|
||||||
|
|
||||||
|
ColorSequence = function(value)
|
||||||
|
local keypoints = {}
|
||||||
|
|
||||||
|
for index, keypoint in ipairs(value.Keypoints) do
|
||||||
|
keypoints[index] = ColorSequenceKeypoint.new(
|
||||||
|
keypoint.Time,
|
||||||
|
Color3.new(unpack(keypoint.Color))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ColorSequence.new(keypoints)
|
||||||
|
end,
|
||||||
|
|
||||||
|
PhysicalProperties = function(properties)
|
||||||
|
if properties == nil then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
return PhysicalProperties.new(
|
||||||
|
properties.Density,
|
||||||
|
properties.Friction,
|
||||||
|
properties.Elasticity,
|
||||||
|
properties.FrictionWeight,
|
||||||
|
properties.ElasticityWeight
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
Ref = function()
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local EncodedValue = {}
|
||||||
|
|
||||||
|
function EncodedValue.decode(encodedValue)
|
||||||
|
local decoder = decoders[encodedValue.Type]
|
||||||
|
if decoder ~= nil then
|
||||||
|
return true, decoder(encodedValue.Value)
|
||||||
|
end
|
||||||
|
|
||||||
|
return false, "Couldn't decode value " .. tostring(encodedValue.Type)
|
||||||
|
end
|
||||||
|
|
||||||
|
function EncodedValue.encode(rbxValue, propertyType)
|
||||||
|
assert(propertyType ~= nil, "Property type descriptor is required")
|
||||||
|
|
||||||
|
if propertyType.type == "Data" then
|
||||||
|
local encoder = encoders[propertyType.name]
|
||||||
|
|
||||||
|
if encoder == nil then
|
||||||
|
return false, ("Missing encoder for property type %q"):format(propertyType.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
if encoder ~= nil then
|
||||||
|
return true, {
|
||||||
|
Type = propertyType.name,
|
||||||
|
Value = encoder(rbxValue),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif propertyType.type == "Enum" then
|
||||||
|
return true, {
|
||||||
|
Type = "Enum",
|
||||||
|
Value = rbxValue.Value,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return false, ("Unknown property descriptor type %q"):format(tostring(propertyType.type))
|
||||||
|
end
|
||||||
|
|
||||||
|
return EncodedValue
|
||||||
127
plugin/rbx_dom_lua/src/EncodedValue.spec.lua
Normal file
127
plugin/rbx_dom_lua/src/EncodedValue.spec.lua
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
return function()
|
||||||
|
local RbxDom = require(script.Parent)
|
||||||
|
local EncodedValue = require(script.Parent.EncodedValue)
|
||||||
|
|
||||||
|
it("should decode Rect values", function()
|
||||||
|
local input = {
|
||||||
|
Type = "Rect",
|
||||||
|
Value = {
|
||||||
|
Min = {1, 2},
|
||||||
|
Max = {3, 4},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local output = Rect.new(1, 2, 3, 4)
|
||||||
|
|
||||||
|
local ok, decoded = EncodedValue.decode(input)
|
||||||
|
|
||||||
|
assert(ok, decoded)
|
||||||
|
expect(decoded).to.equal(output)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should decode ColorSequence values", function()
|
||||||
|
local input = {
|
||||||
|
Type = "ColorSequence",
|
||||||
|
Value = {
|
||||||
|
Keypoints = {
|
||||||
|
{
|
||||||
|
Time = 0,
|
||||||
|
Color = { 0.12, 0.34, 0.56 },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Time = 1,
|
||||||
|
Color = { 0.13, 0.33, 0.37 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local output = ColorSequence.new({
|
||||||
|
ColorSequenceKeypoint.new(0, Color3.new(0.12, 0.34, 0.56)),
|
||||||
|
ColorSequenceKeypoint.new(1, Color3.new(0.13, 0.33, 0.37)),
|
||||||
|
})
|
||||||
|
|
||||||
|
local ok, decoded = EncodedValue.decode(input)
|
||||||
|
assert(ok, decoded)
|
||||||
|
expect(decoded).to.equal(output)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should decode NumberSequence values", function()
|
||||||
|
local input = {
|
||||||
|
Type = "NumberSequence",
|
||||||
|
Value = {
|
||||||
|
Keypoints = {
|
||||||
|
{
|
||||||
|
Time = 0,
|
||||||
|
Value = 0.5,
|
||||||
|
Envelope = 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Time = 1,
|
||||||
|
Value = 0.5,
|
||||||
|
Envelope = 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local output = NumberSequence.new({
|
||||||
|
NumberSequenceKeypoint.new(0, 0.5, 0),
|
||||||
|
NumberSequenceKeypoint.new(1, 0.5, 0),
|
||||||
|
})
|
||||||
|
|
||||||
|
local ok, decoded = EncodedValue.decode(input)
|
||||||
|
assert(ok, decoded)
|
||||||
|
expect(decoded).to.equal(output)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should decode PhysicalProperties values", function()
|
||||||
|
local input = {
|
||||||
|
Type = "PhysicalProperties",
|
||||||
|
Value = {
|
||||||
|
Density = 0.1,
|
||||||
|
Friction = 0.2,
|
||||||
|
Elasticity = 0.3,
|
||||||
|
FrictionWeight = 0.4,
|
||||||
|
ElasticityWeight = 0.5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local output = PhysicalProperties.new(
|
||||||
|
0.1,
|
||||||
|
0.2,
|
||||||
|
0.3,
|
||||||
|
0.4,
|
||||||
|
0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
local ok, decoded = EncodedValue.decode(input)
|
||||||
|
assert(ok, decoded)
|
||||||
|
expect(decoded).to.equal(output)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- This part of rbx_dom_lua needs some work still.
|
||||||
|
itSKIP("should encode Rect values", function()
|
||||||
|
local input = Rect.new(10, 20, 30, 40)
|
||||||
|
|
||||||
|
local output = {
|
||||||
|
Type = "Rect",
|
||||||
|
Value = {
|
||||||
|
Min = {10, 20},
|
||||||
|
Max = {30, 40},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local descriptor = RbxDom.findCanonicalPropertyDescriptor("ImageLabel", "SliceCenter")
|
||||||
|
local ok, encoded = EncodedValue.encode(input, descriptor)
|
||||||
|
|
||||||
|
assert(ok, encoded)
|
||||||
|
expect(encoded.Type).to.equal(output.Type)
|
||||||
|
expect(encoded.Value.Min[1]).to.equal(output.Value.Min[1])
|
||||||
|
expect(encoded.Value.Min[2]).to.equal(output.Value.Min[2])
|
||||||
|
expect(encoded.Value.Max[1]).to.equal(output.Value.Max[1])
|
||||||
|
expect(encoded.Value.Max[2]).to.equal(output.Value.Max[2])
|
||||||
|
end)
|
||||||
|
end
|
||||||
28
plugin/rbx_dom_lua/src/Error.lua
Normal file
28
plugin/rbx_dom_lua/src/Error.lua
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
local Error = {}
|
||||||
|
Error.__index = Error
|
||||||
|
|
||||||
|
Error.Kind = {
|
||||||
|
UnknownProperty = "UnknownProperty",
|
||||||
|
PropertyNotReadable = "PropertyNotReadable",
|
||||||
|
PropertyNotWritable = "PropertyNotWritable",
|
||||||
|
Roblox = "Roblox",
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(Error.Kind, {
|
||||||
|
__index = function(_, key)
|
||||||
|
error(("%q is not a valid member of Error.Kind"):format(tostring(key)), 2)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
function Error.new(kind, extra)
|
||||||
|
return setmetatable({
|
||||||
|
kind = kind,
|
||||||
|
extra = extra,
|
||||||
|
}, Error)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Error:__tostring()
|
||||||
|
return ("Error(%s: %s)"):format(self.kind, tostring(self.extra))
|
||||||
|
end
|
||||||
|
|
||||||
|
return Error
|
||||||
80
plugin/rbx_dom_lua/src/PropertyDescriptor.lua
Normal file
80
plugin/rbx_dom_lua/src/PropertyDescriptor.lua
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
local Error = require(script.Parent.Error)
|
||||||
|
local customProperties = require(script.Parent.customProperties)
|
||||||
|
|
||||||
|
-- A wrapper around a property descriptor from the reflection database with some
|
||||||
|
-- extra convenience methods.
|
||||||
|
--
|
||||||
|
-- The aim of this API is to facilitate looking up a property once, then reading
|
||||||
|
-- from it or writing to it multiple times. It's also useful when a consumer
|
||||||
|
-- wants to check additional constraints on the property before trying to use
|
||||||
|
-- it, like scriptability.
|
||||||
|
local PropertyDescriptor = {}
|
||||||
|
PropertyDescriptor.__index = PropertyDescriptor
|
||||||
|
|
||||||
|
local function get(container, key)
|
||||||
|
return container[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function set(container, key, value)
|
||||||
|
container[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
function PropertyDescriptor.fromRaw(data, className, propertyName)
|
||||||
|
return setmetatable({
|
||||||
|
scriptability = data.scriptability,
|
||||||
|
className = className,
|
||||||
|
name = propertyName,
|
||||||
|
}, PropertyDescriptor)
|
||||||
|
end
|
||||||
|
|
||||||
|
function PropertyDescriptor:read(instance)
|
||||||
|
if self.scriptability == "ReadWrite" or self.scriptability == "Read" then
|
||||||
|
local success, value = xpcall(get, debug.traceback, instance, self.name)
|
||||||
|
|
||||||
|
if success then
|
||||||
|
return success, value
|
||||||
|
else
|
||||||
|
return false, Error.new(Error.Kind.Roblox, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.scriptability == "Custom" then
|
||||||
|
local interface = customProperties[self.className][self.name]
|
||||||
|
|
||||||
|
return interface.read(instance, self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.scriptability == "None" or self.scriptability == "Write" then
|
||||||
|
local fullName = ("%s.%s"):format(instance.className, self.name)
|
||||||
|
|
||||||
|
return false, Error.new(Error.Kind.PropertyNotReadable, fullName)
|
||||||
|
end
|
||||||
|
|
||||||
|
error(("Internal error: unexpected value of 'scriptability': %s"):format(tostring(self.scriptability)), 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
function PropertyDescriptor:write(instance, value)
|
||||||
|
if self.scriptability == "ReadWrite" or self.scriptability == "Write" then
|
||||||
|
local success, err = xpcall(set, debug.traceback, instance, self.name, value)
|
||||||
|
|
||||||
|
if success then
|
||||||
|
return success
|
||||||
|
else
|
||||||
|
return false, Error.new(Error.Kind.Roblox, err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.scriptability == "Custom" then
|
||||||
|
local interface = customProperties[self.className][self.name]
|
||||||
|
|
||||||
|
return interface.write(instance, self.name, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.scriptability == "None" or self.scriptability == "Read" then
|
||||||
|
local fullName = ("%s.%s"):format(instance.className, self.name)
|
||||||
|
|
||||||
|
return false, Error.new(Error.Kind.PropertyNotWritable, fullName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return PropertyDescriptor
|
||||||
18757
plugin/rbx_dom_lua/src/ReflectionDatabase/classes.lua
Normal file
18757
plugin/rbx_dom_lua/src/ReflectionDatabase/classes.lua
Normal file
File diff suppressed because it is too large
Load Diff
3
plugin/rbx_dom_lua/src/ReflectionDatabase/init.lua
Normal file
3
plugin/rbx_dom_lua/src/ReflectionDatabase/init.lua
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
return {
|
||||||
|
classes = require(script.classes)
|
||||||
|
}
|
||||||
139
plugin/rbx_dom_lua/src/base64.lua
Normal file
139
plugin/rbx_dom_lua/src/base64.lua
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
-- Thanks to Tiffany352 for this base64 implementation!
|
||||||
|
|
||||||
|
local floor = math.floor
|
||||||
|
local char = string.char
|
||||||
|
|
||||||
|
local function encodeBase64(str)
|
||||||
|
local out = {}
|
||||||
|
local nOut = 0
|
||||||
|
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
|
local strLen = #str
|
||||||
|
|
||||||
|
-- 3 octets become 4 hextets
|
||||||
|
for i = 1, strLen - 2, 3 do
|
||||||
|
local b1, b2, b3 = str:byte(i, i + 3)
|
||||||
|
local word = b3 + b2 * 256 + b1 * 256 * 256
|
||||||
|
|
||||||
|
local h4 = word % 64 + 1
|
||||||
|
word = floor(word / 64)
|
||||||
|
local h3 = word % 64 + 1
|
||||||
|
word = floor(word / 64)
|
||||||
|
local h2 = word % 64 + 1
|
||||||
|
word = floor(word / 64)
|
||||||
|
local h1 = word % 64 + 1
|
||||||
|
|
||||||
|
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||||
|
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||||
|
out[nOut + 3] = alphabet:sub(h3, h3)
|
||||||
|
out[nOut + 4] = alphabet:sub(h4, h4)
|
||||||
|
nOut = nOut + 4
|
||||||
|
end
|
||||||
|
|
||||||
|
local remainder = strLen % 3
|
||||||
|
|
||||||
|
if remainder == 2 then
|
||||||
|
-- 16 input bits -> 3 hextets (2 full, 1 partial)
|
||||||
|
local b1, b2 = str:byte(-2, -1)
|
||||||
|
-- partial is 4 bits long, leaving 2 bits of zero padding ->
|
||||||
|
-- offset = 4
|
||||||
|
local word = b2 * 4 + b1 * 4 * 256
|
||||||
|
|
||||||
|
local h3 = word % 64 + 1
|
||||||
|
word = floor(word / 64)
|
||||||
|
local h2 = word % 64 + 1
|
||||||
|
word = floor(word / 64)
|
||||||
|
local h1 = word % 64 + 1
|
||||||
|
|
||||||
|
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||||
|
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||||
|
out[nOut + 3] = alphabet:sub(h3, h3)
|
||||||
|
out[nOut + 4] = "="
|
||||||
|
elseif remainder == 1 then
|
||||||
|
-- 8 input bits -> 2 hextets (2 full, 1 partial)
|
||||||
|
local b1 = str:byte(-1, -1)
|
||||||
|
-- partial is 2 bits long, leaving 4 bits of zero padding ->
|
||||||
|
-- offset = 16
|
||||||
|
local word = b1 * 16
|
||||||
|
|
||||||
|
local h2 = word % 64 + 1
|
||||||
|
word = floor(word / 64)
|
||||||
|
local h1 = word % 64 + 1
|
||||||
|
|
||||||
|
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||||
|
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||||
|
out[nOut + 3] = "="
|
||||||
|
out[nOut + 4] = "="
|
||||||
|
end
|
||||||
|
-- if the remainder is 0, then no work is needed
|
||||||
|
|
||||||
|
return table.concat(out, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function decodeBase64(str)
|
||||||
|
local out = {}
|
||||||
|
local nOut = 0
|
||||||
|
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
|
local strLen = #str
|
||||||
|
local acc = 0
|
||||||
|
local nAcc = 0
|
||||||
|
|
||||||
|
local alphabetLut = {}
|
||||||
|
for i = 1, #alphabet do
|
||||||
|
alphabetLut[alphabet:sub(i, i)] = i - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 4 hextets become 3 octets
|
||||||
|
for i = 1, strLen do
|
||||||
|
local ch = str:sub(i, i)
|
||||||
|
local byte = alphabetLut[ch]
|
||||||
|
if byte then
|
||||||
|
acc = acc * 64 + byte
|
||||||
|
nAcc = nAcc + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if nAcc == 4 then
|
||||||
|
local b3 = acc % 256
|
||||||
|
acc = floor(acc / 256)
|
||||||
|
local b2 = acc % 256
|
||||||
|
acc = floor(acc / 256)
|
||||||
|
local b1 = acc % 256
|
||||||
|
|
||||||
|
out[nOut + 1] = char(b1)
|
||||||
|
out[nOut + 2] = char(b2)
|
||||||
|
out[nOut + 3] = char(b3)
|
||||||
|
nOut = nOut + 3
|
||||||
|
nAcc = 0
|
||||||
|
acc = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if nAcc == 3 then
|
||||||
|
-- 3 hextets -> 16 bit output
|
||||||
|
acc = acc * 64
|
||||||
|
acc = floor(acc / 256)
|
||||||
|
local b2 = acc % 256
|
||||||
|
acc = floor(acc / 256)
|
||||||
|
local b1 = acc % 256
|
||||||
|
|
||||||
|
out[nOut + 1] = char(b1)
|
||||||
|
out[nOut + 2] = char(b2)
|
||||||
|
elseif nAcc == 2 then
|
||||||
|
-- 2 hextets -> 8 bit output
|
||||||
|
acc = acc * 64
|
||||||
|
acc = floor(acc / 256)
|
||||||
|
acc = acc * 64
|
||||||
|
acc = floor(acc / 256)
|
||||||
|
local b1 = acc % 256
|
||||||
|
|
||||||
|
out[nOut + 1] = char(b1)
|
||||||
|
elseif nAcc == 1 then
|
||||||
|
error("Base64 has invalid length")
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(out, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
decode = decodeBase64,
|
||||||
|
encode = encodeBase64,
|
||||||
|
}
|
||||||
29
plugin/rbx_dom_lua/src/base64.spec.lua
Normal file
29
plugin/rbx_dom_lua/src/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
|
||||||
47
plugin/rbx_dom_lua/src/customProperties.lua
Normal file
47
plugin/rbx_dom_lua/src/customProperties.lua
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
local CollectionService = game:GetService("CollectionService")
|
||||||
|
|
||||||
|
-- Defines how to read and write properties that aren't directly scriptable.
|
||||||
|
--
|
||||||
|
-- The reflection database refers to these as having scriptability = "Custom"
|
||||||
|
return {
|
||||||
|
Instance = {
|
||||||
|
Tags = {
|
||||||
|
read = function(instance, key)
|
||||||
|
local tagList = CollectionService:GetTags(instance)
|
||||||
|
|
||||||
|
return true, table.concat(tagList, "\0")
|
||||||
|
end,
|
||||||
|
write = function(instance, key, value)
|
||||||
|
local existingTags = CollectionService:GetTags(instance)
|
||||||
|
|
||||||
|
local unseenTags = {}
|
||||||
|
for _, tag in ipairs(existingTags) do
|
||||||
|
unseenTags[tag] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local tagList = string.split(value, "\0")
|
||||||
|
for _, tag in ipairs(tagList) do
|
||||||
|
unseenTags[tag] = nil
|
||||||
|
CollectionService:AddTag(instance, tag)
|
||||||
|
end
|
||||||
|
|
||||||
|
for tag in pairs(unseenTags) do
|
||||||
|
CollectionService:RemoveTag(instance, tag)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LocalizationTable = {
|
||||||
|
Contents = {
|
||||||
|
read = function(instance, key)
|
||||||
|
return true, instance:GetContents()
|
||||||
|
end,
|
||||||
|
write = function(instance, key, value)
|
||||||
|
instance:SetContents(value)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
67
plugin/rbx_dom_lua/src/init.lua
Normal file
67
plugin/rbx_dom_lua/src/init.lua
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
local ReflectionDatabase = require(script.ReflectionDatabase)
|
||||||
|
local Error = require(script.Error)
|
||||||
|
local PropertyDescriptor = require(script.PropertyDescriptor)
|
||||||
|
|
||||||
|
local function findCanonicalPropertyDescriptor(className, propertyName)
|
||||||
|
local currentClassName = className
|
||||||
|
|
||||||
|
repeat
|
||||||
|
local currentClass = ReflectionDatabase.classes[currentClassName]
|
||||||
|
|
||||||
|
if currentClass == nil then
|
||||||
|
return currentClass
|
||||||
|
end
|
||||||
|
|
||||||
|
local propertyData = currentClass.properties[propertyName]
|
||||||
|
if propertyData ~= nil then
|
||||||
|
if propertyData.isCanonical then
|
||||||
|
return PropertyDescriptor.fromRaw(propertyData, currentClassName, propertyName)
|
||||||
|
end
|
||||||
|
|
||||||
|
if propertyData.canonicalName ~= nil then
|
||||||
|
return PropertyDescriptor.fromRaw(
|
||||||
|
currentClass.properties[propertyData.canonicalName],
|
||||||
|
currentClassName,
|
||||||
|
propertyData.canonicalName)
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
currentClassName = currentClass.superclass
|
||||||
|
until currentClassName == nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readProperty(instance, propertyName)
|
||||||
|
local descriptor = findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
|
||||||
|
|
||||||
|
if descriptor == nil then
|
||||||
|
local fullName = ("%s.%s"):format(instance.className, propertyName)
|
||||||
|
|
||||||
|
return false, Error.new(Error.Kind.UnknownProperty, fullName)
|
||||||
|
end
|
||||||
|
|
||||||
|
return descriptor:read(instance)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function writeProperty(instance, propertyName, value)
|
||||||
|
local descriptor = findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
|
||||||
|
|
||||||
|
if descriptor == nil then
|
||||||
|
local fullName = ("%s.%s"):format(instance.className, propertyName)
|
||||||
|
|
||||||
|
return false, Error.new(Error.Kind.UnknownProperty, fullName)
|
||||||
|
end
|
||||||
|
|
||||||
|
return descriptor:write(instance, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
readProperty = readProperty,
|
||||||
|
writeProperty = writeProperty,
|
||||||
|
findCanonicalPropertyDescriptor = findCanonicalPropertyDescriptor,
|
||||||
|
Error = Error,
|
||||||
|
EncodedValue = require(script.EncodedValue),
|
||||||
|
}
|
||||||
7
plugin/rbx_dom_lua/src/init.spec.lua
Normal file
7
plugin/rbx_dom_lua/src/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
|
||||||
35
plugin/rbx_dom_lua/test-place.project.json
Normal file
35
plugin/rbx_dom_lua/test-place.project.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "rbx_dom_lua test place",
|
||||||
|
"tree": {
|
||||||
|
"$className": "DataModel",
|
||||||
|
"ReplicatedStorage": {
|
||||||
|
"$className": "ReplicatedStorage",
|
||||||
|
|
||||||
|
"RbxDom": {
|
||||||
|
"$path": "src"
|
||||||
|
},
|
||||||
|
"TestEZ": {
|
||||||
|
"$path": "modules/testez/lib"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ServerScriptService": {
|
||||||
|
"$className": "ServerScriptService",
|
||||||
|
|
||||||
|
"Run Tests": {
|
||||||
|
"$path": "test.server.lua"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Players": {
|
||||||
|
"$className": "Players",
|
||||||
|
"$properties": {
|
||||||
|
"CharacterAutoLoads": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HttpService": {
|
||||||
|
"$className": "HttpService",
|
||||||
|
"$properties": {
|
||||||
|
"HttpEnabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
plugin/rbx_dom_lua/test.server.lua
Normal file
7
plugin/rbx_dom_lua/test.server.lua
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
|
||||||
|
local LIB_ROOT = ReplicatedStorage.RbxDom
|
||||||
|
|
||||||
|
local TestEZ = require(ReplicatedStorage.TestEZ)
|
||||||
|
|
||||||
|
TestEZ.TestBootstrap:run({LIB_ROOT})
|
||||||
@@ -233,4 +233,19 @@ function ApiContext:retrieveMessages()
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function ApiContext:open(id)
|
||||||
|
local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
|
||||||
|
|
||||||
|
return Http.post(url, "")
|
||||||
|
:andThen(rejectFailedRequests)
|
||||||
|
:andThen(Http.Response.json)
|
||||||
|
:andThen(function(body)
|
||||||
|
if body.sessionId ~= self.__sessionId then
|
||||||
|
return Promise.reject("Server changed ID")
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
return ApiContext
|
return ApiContext
|
||||||
@@ -17,6 +17,7 @@ local ConnectPanel = require(Plugin.Components.ConnectPanel)
|
|||||||
local ConnectingPanel = require(Plugin.Components.ConnectingPanel)
|
local ConnectingPanel = require(Plugin.Components.ConnectingPanel)
|
||||||
local ConnectionActivePanel = require(Plugin.Components.ConnectionActivePanel)
|
local ConnectionActivePanel = require(Plugin.Components.ConnectionActivePanel)
|
||||||
local ErrorPanel = require(Plugin.Components.ErrorPanel)
|
local ErrorPanel = require(Plugin.Components.ErrorPanel)
|
||||||
|
local SettingsPanel = require(Plugin.Components.SettingsPanel)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ local AppStatus = strict("AppStatus", {
|
|||||||
Connecting = "Connecting",
|
Connecting = "Connecting",
|
||||||
Connected = "Connected",
|
Connected = "Connected",
|
||||||
Error = "Error",
|
Error = "Error",
|
||||||
|
Settings = "Settings",
|
||||||
})
|
})
|
||||||
|
|
||||||
local App = Roact.Component:extend("App")
|
local App = Roact.Component:extend("App")
|
||||||
@@ -73,10 +75,7 @@ function App:init()
|
|||||||
|
|
||||||
self.signals = {}
|
self.signals = {}
|
||||||
self.serveSession = nil
|
self.serveSession = nil
|
||||||
|
self.displayedVersion = Version.display(Config.version)
|
||||||
self.displayedVersion = DevSettings:isEnabled()
|
|
||||||
and Config.codename
|
|
||||||
or Version.display(Config.version)
|
|
||||||
|
|
||||||
local toolbar = self.props.plugin:CreateToolbar("Rojo " .. self.displayedVersion)
|
local toolbar = self.props.plugin:CreateToolbar("Rojo " .. self.displayedVersion)
|
||||||
|
|
||||||
@@ -108,12 +107,14 @@ function App:init()
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:startSession(address, port)
|
function App:startSession(address, port, sessionOptions)
|
||||||
Log.trace("Starting new session")
|
Log.trace("Starting new session")
|
||||||
|
|
||||||
local baseUrl = ("http://%s:%s"):format(address, port)
|
local baseUrl = ("http://%s:%s"):format(address, port)
|
||||||
self.serveSession = ServeSession.new({
|
self.serveSession = ServeSession.new({
|
||||||
apiContext = ApiContext.new(baseUrl),
|
apiContext = ApiContext.new(baseUrl),
|
||||||
|
openScriptsExternally = sessionOptions.openScriptsExternally,
|
||||||
|
twoWaySync = sessionOptions.twoWaySync,
|
||||||
})
|
})
|
||||||
|
|
||||||
self.serveSession:onStatusChanged(function(status, details)
|
self.serveSession:onStatusChanged(function(status, details)
|
||||||
@@ -154,8 +155,13 @@ function App:render()
|
|||||||
if self.state.appStatus == AppStatus.NotStarted then
|
if self.state.appStatus == AppStatus.NotStarted then
|
||||||
children = {
|
children = {
|
||||||
ConnectPanel = e(ConnectPanel, {
|
ConnectPanel = e(ConnectPanel, {
|
||||||
startSession = function(address, port)
|
startSession = function(address, port, settings)
|
||||||
self:startSession(address, port)
|
self:startSession(address, port, settings)
|
||||||
|
end,
|
||||||
|
openSettings = function()
|
||||||
|
self:setState({
|
||||||
|
appStatus = AppStatus.Settings,
|
||||||
|
})
|
||||||
end,
|
end,
|
||||||
cancel = function()
|
cancel = function()
|
||||||
Log.trace("Canceling session configuration")
|
Log.trace("Canceling session configuration")
|
||||||
@@ -168,7 +174,7 @@ function App:render()
|
|||||||
}
|
}
|
||||||
elseif self.state.appStatus == AppStatus.Connecting then
|
elseif self.state.appStatus == AppStatus.Connecting then
|
||||||
children = {
|
children = {
|
||||||
ConnectingPanel = Roact.createElement(ConnectingPanel),
|
ConnectingPanel = e(ConnectingPanel),
|
||||||
}
|
}
|
||||||
elseif self.state.appStatus == AppStatus.Connected then
|
elseif self.state.appStatus == AppStatus.Connected then
|
||||||
children = {
|
children = {
|
||||||
@@ -186,9 +192,19 @@ function App:render()
|
|||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
elseif self.state.appStatus == AppStatus.Settings then
|
||||||
|
children = {
|
||||||
|
e(SettingsPanel, {
|
||||||
|
back = function()
|
||||||
|
self:setState({
|
||||||
|
appStatus = AppStatus.NotStarted,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
}
|
||||||
elseif self.state.appStatus == AppStatus.Error then
|
elseif self.state.appStatus == AppStatus.Error then
|
||||||
children = {
|
children = {
|
||||||
ErrorPanel = Roact.createElement(ErrorPanel, {
|
ErrorPanel = e(ErrorPanel, {
|
||||||
errorMessage = self.state.errorMessage,
|
errorMessage = self.state.errorMessage,
|
||||||
onDismiss = function()
|
onDismiss = function()
|
||||||
self:setState({
|
self:setState({
|
||||||
@@ -199,7 +215,7 @@ function App:render()
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
return Roact.createElement(Roact.Portal, {
|
return e(Roact.Portal, {
|
||||||
target = self.dockWidget,
|
target = self.dockWidget,
|
||||||
}, children)
|
}, children)
|
||||||
end
|
end
|
||||||
|
|||||||
39
plugin/src/Components/Checkbox.lua
Normal file
39
plugin/src/Components/Checkbox.lua
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
|
||||||
|
local Roact = require(Rojo.Roact)
|
||||||
|
|
||||||
|
local Theme = require(Plugin.Components.Theme)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local function Checkbox(props)
|
||||||
|
local checked = props.checked
|
||||||
|
local layoutOrder = props.layoutOrder
|
||||||
|
local onChange = props.onChange
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
return e("ImageButton", {
|
||||||
|
LayoutOrder = layoutOrder,
|
||||||
|
Size = UDim2.new(0, 20, 0, 20),
|
||||||
|
BorderSizePixel = 2,
|
||||||
|
BorderColor3 = theme.Text2,
|
||||||
|
BackgroundColor3 = theme.Background2,
|
||||||
|
|
||||||
|
[Roact.Event.Activated] = function()
|
||||||
|
onChange(not checked)
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
Indicator = e("Frame", {
|
||||||
|
Size = UDim2.new(0, 18, 0, 18),
|
||||||
|
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundColor3 = theme.Brand1,
|
||||||
|
BackgroundTransparency = checked and 0 or 1,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Checkbox
|
||||||
@@ -4,13 +4,14 @@ local Plugin = Rojo.Plugin
|
|||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Rojo.Roact)
|
||||||
|
|
||||||
local Config = require(Plugin.Config)
|
local Config = require(Plugin.Config)
|
||||||
local Theme = require(Plugin.Theme)
|
|
||||||
|
|
||||||
|
local Theme = require(Plugin.Components.Theme)
|
||||||
local Panel = require(Plugin.Components.Panel)
|
local Panel = require(Plugin.Components.Panel)
|
||||||
local FitList = require(Plugin.Components.FitList)
|
local FitList = require(Plugin.Components.FitList)
|
||||||
local FitText = require(Plugin.Components.FitText)
|
local FitText = require(Plugin.Components.FitText)
|
||||||
local FormButton = require(Plugin.Components.FormButton)
|
local FormButton = require(Plugin.Components.FormButton)
|
||||||
local FormTextInput = require(Plugin.Components.FormTextInput)
|
local FormTextInput = require(Plugin.Components.FormTextInput)
|
||||||
|
local PluginSettings = require(Plugin.Components.PluginSettings)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -25,137 +26,158 @@ end
|
|||||||
|
|
||||||
function ConnectPanel:render()
|
function ConnectPanel:render()
|
||||||
local startSession = self.props.startSession
|
local startSession = self.props.startSession
|
||||||
|
local openSettings = self.props.openSettings
|
||||||
|
|
||||||
return e(Panel, nil, {
|
return Theme.with(function(theme)
|
||||||
Layout = e("UIListLayout", {
|
return PluginSettings.with(function(settings)
|
||||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
return e(Panel, nil, {
|
||||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
Layout = e("UIListLayout", {
|
||||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
}),
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
Inputs = e(FitList, {
|
|
||||||
containerProps = {
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
LayoutOrder = 1,
|
|
||||||
},
|
|
||||||
layoutProps = {
|
|
||||||
FillDirection = Enum.FillDirection.Horizontal,
|
|
||||||
Padding = UDim.new(0, 8),
|
|
||||||
},
|
|
||||||
paddingProps = {
|
|
||||||
PaddingTop = UDim.new(0, 20),
|
|
||||||
PaddingBottom = UDim.new(0, 10),
|
|
||||||
PaddingLeft = UDim.new(0, 24),
|
|
||||||
PaddingRight = UDim.new(0, 24),
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Address = e(FitList, {
|
|
||||||
containerProps = {
|
|
||||||
LayoutOrder = 1,
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
},
|
|
||||||
layoutProps = {
|
|
||||||
Padding = UDim.new(0, 4),
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Label = e(FitText, {
|
|
||||||
Kind = "TextLabel",
|
|
||||||
LayoutOrder = 1,
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
|
||||||
Font = Theme.TitleFont,
|
|
||||||
TextSize = 20,
|
|
||||||
Text = "Address",
|
|
||||||
TextColor3 = Theme.PrimaryColor,
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Input = e(FormTextInput, {
|
Inputs = e(FitList, {
|
||||||
layoutOrder = 2,
|
containerProps = {
|
||||||
width = UDim.new(0, 220),
|
BackgroundTransparency = 1,
|
||||||
value = self.state.address,
|
LayoutOrder = 1,
|
||||||
placeholderValue = Config.defaultHost,
|
},
|
||||||
onValueChange = function(newValue)
|
layoutProps = {
|
||||||
self:setState({
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
address = newValue,
|
Padding = UDim.new(0, 8),
|
||||||
})
|
},
|
||||||
end,
|
paddingProps = {
|
||||||
}),
|
PaddingTop = UDim.new(0, 20),
|
||||||
}),
|
PaddingBottom = UDim.new(0, 10),
|
||||||
|
PaddingLeft = UDim.new(0, 24),
|
||||||
|
PaddingRight = UDim.new(0, 24),
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Address = e(FitList, {
|
||||||
|
containerProps = {
|
||||||
|
LayoutOrder = 1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
},
|
||||||
|
layoutProps = {
|
||||||
|
Padding = UDim.new(0, 4),
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Label = e(FitText, {
|
||||||
|
Kind = "TextLabel",
|
||||||
|
LayoutOrder = 1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = theme.TitleFont,
|
||||||
|
TextSize = 20,
|
||||||
|
Text = "Address",
|
||||||
|
TextColor3 = theme.Text1,
|
||||||
|
}),
|
||||||
|
|
||||||
Port = e(FitList, {
|
Input = e(FormTextInput, {
|
||||||
containerProps = {
|
layoutOrder = 2,
|
||||||
LayoutOrder = 2,
|
width = UDim.new(0, 220),
|
||||||
BackgroundTransparency = 1,
|
value = self.state.address,
|
||||||
},
|
placeholderValue = Config.defaultHost,
|
||||||
layoutProps = {
|
onValueChange = function(newValue)
|
||||||
Padding = UDim.new(0, 4),
|
self:setState({
|
||||||
},
|
address = newValue,
|
||||||
}, {
|
})
|
||||||
Label = e(FitText, {
|
end,
|
||||||
Kind = "TextLabel",
|
}),
|
||||||
LayoutOrder = 1,
|
}),
|
||||||
BackgroundTransparency = 1,
|
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
Port = e(FitList, {
|
||||||
Font = Theme.TitleFont,
|
containerProps = {
|
||||||
TextSize = 20,
|
LayoutOrder = 2,
|
||||||
Text = "Port",
|
BackgroundTransparency = 1,
|
||||||
TextColor3 = Theme.PrimaryColor,
|
},
|
||||||
|
layoutProps = {
|
||||||
|
Padding = UDim.new(0, 4),
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Label = e(FitText, {
|
||||||
|
Kind = "TextLabel",
|
||||||
|
LayoutOrder = 1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = theme.TitleFont,
|
||||||
|
TextSize = 20,
|
||||||
|
Text = "Port",
|
||||||
|
TextColor3 = theme.Text1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Input = e(FormTextInput, {
|
||||||
|
layoutOrder = 2,
|
||||||
|
width = UDim.new(0, 80),
|
||||||
|
value = self.state.port,
|
||||||
|
placeholderValue = Config.defaultPort,
|
||||||
|
onValueChange = function(newValue)
|
||||||
|
self:setState({
|
||||||
|
port = newValue,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Input = e(FormTextInput, {
|
Buttons = e(FitList, {
|
||||||
layoutOrder = 2,
|
fitAxes = "Y",
|
||||||
width = UDim.new(0, 80),
|
containerProps = {
|
||||||
value = self.state.port,
|
BackgroundTransparency = 1,
|
||||||
placeholderValue = Config.defaultPort,
|
LayoutOrder = 2,
|
||||||
onValueChange = function(newValue)
|
Size = UDim2.new(1, 0, 0, 0),
|
||||||
self:setState({
|
},
|
||||||
port = newValue,
|
layoutProps = {
|
||||||
})
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
end,
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||||
|
Padding = UDim.new(0, 8),
|
||||||
|
},
|
||||||
|
paddingProps = {
|
||||||
|
PaddingTop = UDim.new(0, 0),
|
||||||
|
PaddingBottom = UDim.new(0, 20),
|
||||||
|
PaddingLeft = UDim.new(0, 24),
|
||||||
|
PaddingRight = UDim.new(0, 24),
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
e(FormButton, {
|
||||||
|
layoutOrder = 1,
|
||||||
|
text = "Settings",
|
||||||
|
secondary = true,
|
||||||
|
onClick = function()
|
||||||
|
if openSettings ~= nil then
|
||||||
|
openSettings()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
|
||||||
|
e(FormButton, {
|
||||||
|
layoutOrder = 2,
|
||||||
|
text = "Connect",
|
||||||
|
onClick = function()
|
||||||
|
if startSession ~= nil then
|
||||||
|
local address = self.state.address
|
||||||
|
if address:len() == 0 then
|
||||||
|
address = Config.defaultHost
|
||||||
|
end
|
||||||
|
|
||||||
|
local port = self.state.port
|
||||||
|
if port:len() == 0 then
|
||||||
|
port = Config.defaultPort
|
||||||
|
end
|
||||||
|
|
||||||
|
local sessionOptions = {
|
||||||
|
openScriptsExternally = settings:get("openScriptsExternally"),
|
||||||
|
twoWaySync = settings:get("twoWaySync"),
|
||||||
|
}
|
||||||
|
|
||||||
|
startSession(address, port, sessionOptions)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
})
|
||||||
}),
|
end)
|
||||||
|
end)
|
||||||
Buttons = e(FitList, {
|
|
||||||
fitAxes = "Y",
|
|
||||||
containerProps = {
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
LayoutOrder = 2,
|
|
||||||
Size = UDim2.new(1, 0, 0, 0),
|
|
||||||
},
|
|
||||||
layoutProps = {
|
|
||||||
FillDirection = Enum.FillDirection.Horizontal,
|
|
||||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
|
||||||
Padding = UDim.new(0, 8),
|
|
||||||
},
|
|
||||||
paddingProps = {
|
|
||||||
PaddingTop = UDim.new(0, 0),
|
|
||||||
PaddingBottom = UDim.new(0, 20),
|
|
||||||
PaddingLeft = UDim.new(0, 24),
|
|
||||||
PaddingRight = UDim.new(0, 24),
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
e(FormButton, {
|
|
||||||
layoutOrder = 2,
|
|
||||||
text = "Connect",
|
|
||||||
onClick = function()
|
|
||||||
if startSession ~= nil then
|
|
||||||
local address = self.state.address
|
|
||||||
if address:len() == 0 then
|
|
||||||
address = Config.defaultHost
|
|
||||||
end
|
|
||||||
|
|
||||||
local port = self.state.port
|
|
||||||
if port:len() == 0 then
|
|
||||||
port = Config.defaultPort
|
|
||||||
end
|
|
||||||
|
|
||||||
startSession(address, port)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return ConnectPanel
|
return ConnectPanel
|
||||||
@@ -2,8 +2,7 @@ local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
|||||||
|
|
||||||
local Plugin = script:FindFirstAncestor("Plugin")
|
local Plugin = script:FindFirstAncestor("Plugin")
|
||||||
|
|
||||||
local Theme = require(Plugin.Theme)
|
local Theme = require(Plugin.Components.Theme)
|
||||||
|
|
||||||
local Panel = require(Plugin.Components.Panel)
|
local Panel = require(Plugin.Components.Panel)
|
||||||
local FitText = require(Plugin.Components.FitText)
|
local FitText = require(Plugin.Components.FitText)
|
||||||
|
|
||||||
@@ -12,23 +11,25 @@ local e = Roact.createElement
|
|||||||
local ConnectingPanel = Roact.Component:extend("ConnectingPanel")
|
local ConnectingPanel = Roact.Component:extend("ConnectingPanel")
|
||||||
|
|
||||||
function ConnectingPanel:render()
|
function ConnectingPanel:render()
|
||||||
return e(Panel, nil, {
|
return Theme.with(function(theme)
|
||||||
Layout = Roact.createElement("UIListLayout", {
|
return e(Panel, nil, {
|
||||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
Layout = Roact.createElement("UIListLayout", {
|
||||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
Padding = UDim.new(0, 8),
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
}),
|
Padding = UDim.new(0, 8),
|
||||||
|
}),
|
||||||
|
|
||||||
Text = e(FitText, {
|
Text = e(FitText, {
|
||||||
Padding = Vector2.new(12, 6),
|
Padding = Vector2.new(12, 6),
|
||||||
Font = Theme.ButtonFont,
|
Font = theme.ButtonFont,
|
||||||
TextSize = 18,
|
TextSize = 18,
|
||||||
Text = "Connecting...",
|
Text = "Connecting...",
|
||||||
TextColor3 = Theme.PrimaryColor,
|
TextColor3 = theme.Text1,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return ConnectingPanel
|
return ConnectingPanel
|
||||||
@@ -2,8 +2,7 @@ local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
|||||||
|
|
||||||
local Plugin = script:FindFirstAncestor("Plugin")
|
local Plugin = script:FindFirstAncestor("Plugin")
|
||||||
|
|
||||||
local Theme = require(Plugin.Theme)
|
local Theme = require(Plugin.Components.Theme)
|
||||||
|
|
||||||
local Panel = require(Plugin.Components.Panel)
|
local Panel = require(Plugin.Components.Panel)
|
||||||
local FitText = require(Plugin.Components.FitText)
|
local FitText = require(Plugin.Components.FitText)
|
||||||
local FormButton = require(Plugin.Components.FormButton)
|
local FormButton = require(Plugin.Components.FormButton)
|
||||||
@@ -15,32 +14,34 @@ local ConnectionActivePanel = Roact.Component:extend("ConnectionActivePanel")
|
|||||||
function ConnectionActivePanel:render()
|
function ConnectionActivePanel:render()
|
||||||
local stopSession = self.props.stopSession
|
local stopSession = self.props.stopSession
|
||||||
|
|
||||||
return e(Panel, nil, {
|
return Theme.with(function(theme)
|
||||||
Layout = Roact.createElement("UIListLayout", {
|
return e(Panel, nil, {
|
||||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
Layout = Roact.createElement("UIListLayout", {
|
||||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
Padding = UDim.new(0, 8),
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
}),
|
Padding = UDim.new(0, 8),
|
||||||
|
}),
|
||||||
|
|
||||||
Text = e(FitText, {
|
Text = e(FitText, {
|
||||||
Padding = Vector2.new(12, 6),
|
Padding = Vector2.new(12, 6),
|
||||||
Font = Theme.ButtonFont,
|
Font = theme.ButtonFont,
|
||||||
TextSize = 18,
|
TextSize = 18,
|
||||||
Text = "Connected to Live-Sync Server",
|
Text = "Connected to Live-Sync Server",
|
||||||
TextColor3 = Theme.PrimaryColor,
|
TextColor3 = theme.Text1,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
DisconnectButton = e(FormButton, {
|
DisconnectButton = e(FormButton, {
|
||||||
layoutOrder = 2,
|
layoutOrder = 2,
|
||||||
text = "Disconnect",
|
text = "Disconnect",
|
||||||
secondary = true,
|
secondary = true,
|
||||||
onClick = function()
|
onClick = function()
|
||||||
stopSession()
|
stopSession()
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return ConnectionActivePanel
|
return ConnectionActivePanel
|
||||||
@@ -2,8 +2,7 @@ local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
|||||||
|
|
||||||
local Plugin = script:FindFirstAncestor("Plugin")
|
local Plugin = script:FindFirstAncestor("Plugin")
|
||||||
|
|
||||||
local Theme = require(Plugin.Theme)
|
local Theme = require(Plugin.Components.Theme)
|
||||||
|
|
||||||
local Panel = require(Plugin.Components.Panel)
|
local Panel = require(Plugin.Components.Panel)
|
||||||
local FitText = require(Plugin.Components.FitText)
|
local FitText = require(Plugin.Components.FitText)
|
||||||
local FitScrollingFrame = require(Plugin.Components.FitScrollingFrame)
|
local FitScrollingFrame = require(Plugin.Components.FitScrollingFrame)
|
||||||
@@ -20,50 +19,52 @@ function ErrorPanel:render()
|
|||||||
local errorMessage = self.props.errorMessage
|
local errorMessage = self.props.errorMessage
|
||||||
local onDismiss = self.props.onDismiss
|
local onDismiss = self.props.onDismiss
|
||||||
|
|
||||||
return e(Panel, nil, {
|
return Theme.with(function(theme)
|
||||||
Layout = Roact.createElement("UIListLayout", {
|
return e(Panel, nil, {
|
||||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
Layout = Roact.createElement("UIListLayout", {
|
||||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
Padding = UDim.new(0, 8),
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
}),
|
Padding = UDim.new(0, 8),
|
||||||
|
|
||||||
ErrorContainer = e(FitScrollingFrame, {
|
|
||||||
containerProps = {
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
BorderSizePixel = 0,
|
|
||||||
Size = UDim2.new(1, -HOR_PADDING * 2, 1, -BUTTON_HEIGHT),
|
|
||||||
Position = UDim2.new(0, HOR_PADDING, 0, 0),
|
|
||||||
ScrollBarImageColor3 = Theme.PrimaryColor,
|
|
||||||
VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
|
|
||||||
ScrollingDirection = Enum.ScrollingDirection.Y,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Text = e(FitText, {
|
|
||||||
Size = UDim2.new(1, 0, 0, 0),
|
|
||||||
|
|
||||||
LayoutOrder = 1,
|
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
|
||||||
TextYAlignment = Enum.TextYAlignment.Top,
|
|
||||||
FitAxis = "Y",
|
|
||||||
Font = Theme.ButtonFont,
|
|
||||||
TextSize = 18,
|
|
||||||
Text = errorMessage,
|
|
||||||
TextWrap = true,
|
|
||||||
TextColor3 = Theme.PrimaryColor,
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
}),
|
}),
|
||||||
}),
|
|
||||||
|
|
||||||
DismissButton = e(FormButton, {
|
ErrorContainer = e(FitScrollingFrame, {
|
||||||
layoutOrder = 2,
|
containerProps = {
|
||||||
text = "Dismiss",
|
BackgroundTransparency = 1,
|
||||||
secondary = true,
|
BorderSizePixel = 0,
|
||||||
onClick = function()
|
Size = UDim2.new(1, -HOR_PADDING * 2, 1, -BUTTON_HEIGHT),
|
||||||
onDismiss()
|
Position = UDim2.new(0, HOR_PADDING, 0, 0),
|
||||||
end,
|
ScrollBarImageColor3 = theme.Text1,
|
||||||
}),
|
VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
|
||||||
})
|
ScrollingDirection = Enum.ScrollingDirection.Y,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Text = e(FitText, {
|
||||||
|
Size = UDim2.new(1, 0, 0, 0),
|
||||||
|
|
||||||
|
LayoutOrder = 1,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextYAlignment = Enum.TextYAlignment.Top,
|
||||||
|
FitAxis = "Y",
|
||||||
|
Font = theme.ButtonFont,
|
||||||
|
TextSize = 18,
|
||||||
|
Text = errorMessage,
|
||||||
|
TextWrap = true,
|
||||||
|
TextColor3 = theme.Text1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
DismissButton = e(FormButton, {
|
||||||
|
layoutOrder = 2,
|
||||||
|
text = "Dismiss",
|
||||||
|
secondary = true,
|
||||||
|
onClick = function()
|
||||||
|
onDismiss()
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return ErrorPanel
|
return ErrorPanel
|
||||||
@@ -4,7 +4,7 @@ local Plugin = Rojo.Plugin
|
|||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Rojo.Roact)
|
||||||
|
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local Theme = require(Plugin.Theme)
|
local Theme = require(Plugin.Components.Theme)
|
||||||
local FitList = require(Plugin.Components.FitList)
|
local FitList = require(Plugin.Components.FitList)
|
||||||
local FitText = require(Plugin.Components.FitText)
|
local FitText = require(Plugin.Components.FitText)
|
||||||
|
|
||||||
@@ -20,43 +20,45 @@ local function FormButton(props)
|
|||||||
local textColor
|
local textColor
|
||||||
local backgroundColor
|
local backgroundColor
|
||||||
|
|
||||||
if props.secondary then
|
return Theme.with(function(theme)
|
||||||
textColor = Theme.AccentColor
|
if props.secondary then
|
||||||
backgroundColor = Theme.SecondaryColor
|
textColor = theme.Brand1
|
||||||
else
|
backgroundColor = theme.Background2
|
||||||
textColor = Theme.SecondaryColor
|
else
|
||||||
backgroundColor = Theme.AccentColor
|
textColor = theme.TextOnAccent
|
||||||
end
|
backgroundColor = theme.Brand1
|
||||||
|
end
|
||||||
|
|
||||||
return e(FitList, {
|
return e(FitList, {
|
||||||
containerKind = "ImageButton",
|
containerKind = "ImageButton",
|
||||||
containerProps = {
|
containerProps = {
|
||||||
LayoutOrder = layoutOrder,
|
LayoutOrder = layoutOrder,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Image = RoundBox.asset,
|
Image = RoundBox.asset,
|
||||||
ImageRectOffset = RoundBox.offset,
|
ImageRectOffset = RoundBox.offset,
|
||||||
ImageRectSize = RoundBox.size,
|
ImageRectSize = RoundBox.size,
|
||||||
SliceCenter = RoundBox.center,
|
SliceCenter = RoundBox.center,
|
||||||
ScaleType = Enum.ScaleType.Slice,
|
ScaleType = Enum.ScaleType.Slice,
|
||||||
ImageColor3 = backgroundColor,
|
ImageColor3 = backgroundColor,
|
||||||
|
|
||||||
[Roact.Event.Activated] = function()
|
[Roact.Event.Activated] = function()
|
||||||
if onClick ~= nil then
|
if onClick ~= nil then
|
||||||
onClick()
|
onClick()
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
Text = e(FitText, {
|
Text = e(FitText, {
|
||||||
Kind = "TextLabel",
|
Kind = "TextLabel",
|
||||||
Text = text,
|
Text = text,
|
||||||
TextSize = 18,
|
TextSize = 18,
|
||||||
TextColor3 = textColor,
|
TextColor3 = textColor,
|
||||||
Font = Theme.ButtonFont,
|
Font = theme.ButtonFont,
|
||||||
Padding = Vector2.new(16, 8),
|
Padding = Vector2.new(16, 8),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return FormButton
|
return FormButton
|
||||||
@@ -4,7 +4,7 @@ local Plugin = Rojo.Plugin
|
|||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Rojo.Roact)
|
||||||
|
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local Theme = require(Plugin.Theme)
|
local Theme = require(Plugin.Components.Theme)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -35,46 +35,48 @@ function FormTextInput:render()
|
|||||||
shownPlaceholder = placeholderValue
|
shownPlaceholder = placeholderValue
|
||||||
end
|
end
|
||||||
|
|
||||||
return e("ImageLabel", {
|
return Theme.with(function(theme)
|
||||||
LayoutOrder = layoutOrder,
|
return e("ImageLabel", {
|
||||||
Image = RoundBox.asset,
|
LayoutOrder = layoutOrder,
|
||||||
ImageRectOffset = RoundBox.offset,
|
Image = RoundBox.asset,
|
||||||
ImageRectSize = RoundBox.size,
|
ImageRectOffset = RoundBox.offset,
|
||||||
ScaleType = Enum.ScaleType.Slice,
|
ImageRectSize = RoundBox.size,
|
||||||
SliceCenter = RoundBox.center,
|
ScaleType = Enum.ScaleType.Slice,
|
||||||
ImageColor3 = Theme.SecondaryColor,
|
SliceCenter = RoundBox.center,
|
||||||
Size = UDim2.new(width.Scale, width.Offset, 0, TEXT_SIZE + PADDING * 2),
|
ImageColor3 = theme.Background2,
|
||||||
BackgroundTransparency = 1,
|
Size = UDim2.new(width.Scale, width.Offset, 0, TEXT_SIZE + PADDING * 2),
|
||||||
}, {
|
|
||||||
InputInner = e("TextBox", {
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Size = UDim2.new(1, -PADDING * 2, 1, -PADDING * 2),
|
}, {
|
||||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
InputInner = e("TextBox", {
|
||||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
BackgroundTransparency = 1,
|
||||||
Font = Theme.InputFont,
|
Size = UDim2.new(1, -PADDING * 2, 1, -PADDING * 2),
|
||||||
ClearTextOnFocus = false,
|
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||||
TextXAlignment = Enum.TextXAlignment.Center,
|
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||||
TextSize = TEXT_SIZE,
|
Font = theme.InputFont,
|
||||||
Text = value,
|
ClearTextOnFocus = false,
|
||||||
PlaceholderText = shownPlaceholder,
|
TextXAlignment = Enum.TextXAlignment.Center,
|
||||||
PlaceholderColor3 = Theme.LightTextColor,
|
TextSize = TEXT_SIZE,
|
||||||
TextColor3 = Theme.PrimaryColor,
|
Text = value,
|
||||||
|
PlaceholderText = shownPlaceholder,
|
||||||
|
PlaceholderColor3 = theme.Text2,
|
||||||
|
TextColor3 = theme.Text1,
|
||||||
|
|
||||||
[Roact.Change.Text] = function(rbx)
|
[Roact.Change.Text] = function(rbx)
|
||||||
onValueChange(rbx.Text)
|
onValueChange(rbx.Text)
|
||||||
end,
|
end,
|
||||||
[Roact.Event.Focused] = function()
|
[Roact.Event.Focused] = function()
|
||||||
self:setState({
|
self:setState({
|
||||||
focused = true,
|
focused = true,
|
||||||
})
|
})
|
||||||
end,
|
end,
|
||||||
[Roact.Event.FocusLost] = function()
|
[Roact.Event.FocusLost] = function()
|
||||||
self:setState({
|
self:setState({
|
||||||
focused = false,
|
focused = false,
|
||||||
})
|
})
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return FormTextInput
|
return FormTextInput
|
||||||
@@ -3,6 +3,7 @@ local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
|||||||
local Plugin = script:FindFirstAncestor("Plugin")
|
local Plugin = script:FindFirstAncestor("Plugin")
|
||||||
|
|
||||||
local RojoFooter = require(Plugin.Components.RojoFooter)
|
local RojoFooter = require(Plugin.Components.RojoFooter)
|
||||||
|
local Theme = require(Plugin.Components.Theme)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -13,22 +14,25 @@ function Panel:init()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Panel:render()
|
function Panel:render()
|
||||||
return e("Frame", {
|
return Theme.with(function(theme)
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
return e("Frame", {
|
||||||
BackgroundTransparency = 1,
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
}, {
|
BackgroundColor3 = theme.Background1,
|
||||||
Layout = Roact.createElement("UIListLayout", {
|
BorderSizePixel = 1,
|
||||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
}, {
|
||||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
Layout = Roact.createElement("UIListLayout", {
|
||||||
}),
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
}),
|
||||||
|
|
||||||
Body = e("Frame", {
|
Body = e("Frame", {
|
||||||
Size = UDim2.new(0, 360, 1, -32),
|
Size = UDim2.new(0, 360, 1, -32),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, self.props[Roact.Children]),
|
}, self.props[Roact.Children]),
|
||||||
|
|
||||||
Footer = e(RojoFooter),
|
Footer = e(RojoFooter),
|
||||||
})
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return Panel
|
return Panel
|
||||||
121
plugin/src/Components/PluginSettings.lua
Normal file
121
plugin/src/Components/PluginSettings.lua
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
--[[
|
||||||
|
Persistent plugin settings that can be accessed via Roact context.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
|
||||||
|
local Roact = require(Rojo.Roact)
|
||||||
|
|
||||||
|
local defaultSettings = {
|
||||||
|
openScriptsExternally = false,
|
||||||
|
twoWaySync = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
local Settings = {}
|
||||||
|
Settings.__index = Settings
|
||||||
|
|
||||||
|
function Settings.fromPlugin(plugin)
|
||||||
|
local values = {}
|
||||||
|
|
||||||
|
for name, defaultValue in pairs(defaultSettings) do
|
||||||
|
local savedValue = plugin:GetSetting("Rojo_" .. name)
|
||||||
|
|
||||||
|
if savedValue == nil then
|
||||||
|
plugin:SetSetting("Rojo_" .. name, defaultValue)
|
||||||
|
values[name] = defaultValue
|
||||||
|
else
|
||||||
|
values[name] = savedValue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return setmetatable({
|
||||||
|
__values = values,
|
||||||
|
__plugin = plugin,
|
||||||
|
__updateListeners = {},
|
||||||
|
}, Settings)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Settings:get(name)
|
||||||
|
if defaultSettings[name] == nil then
|
||||||
|
error("Invalid setings name " .. tostring(name), 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return self.__values[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
function Settings:set(name, value)
|
||||||
|
self.__plugin:SetSetting("Rojo_" .. name, value)
|
||||||
|
self.__values[name] = value
|
||||||
|
|
||||||
|
for callback in pairs(self.__updateListeners) do
|
||||||
|
callback(name, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Settings:onUpdate(newCallback)
|
||||||
|
local newListeners = {}
|
||||||
|
for callback in pairs(self.__updateListeners) do
|
||||||
|
newListeners[callback] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
newListeners[newCallback] = true
|
||||||
|
self.__updateListeners = newListeners
|
||||||
|
|
||||||
|
return function()
|
||||||
|
local newListeners = {}
|
||||||
|
for callback in pairs(self.__updateListeners) do
|
||||||
|
if callback ~= newCallback then
|
||||||
|
newListeners[callback] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.__updateListeners = newListeners
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local Context = Roact.createContext(nil)
|
||||||
|
|
||||||
|
local StudioProvider = Roact.Component:extend("StudioProvider")
|
||||||
|
|
||||||
|
function StudioProvider:init()
|
||||||
|
self.settings = Settings.fromPlugin(self.props.plugin)
|
||||||
|
end
|
||||||
|
|
||||||
|
function StudioProvider:render()
|
||||||
|
return Roact.createElement(Context.Provider, {
|
||||||
|
value = self.settings,
|
||||||
|
}, self.props[Roact.Children])
|
||||||
|
end
|
||||||
|
|
||||||
|
local InternalConsumer = Roact.Component:extend("InternalConsumer")
|
||||||
|
|
||||||
|
function InternalConsumer:render()
|
||||||
|
return self.props.render(self.props.settings)
|
||||||
|
end
|
||||||
|
|
||||||
|
function InternalConsumer:didMount()
|
||||||
|
self.disconnect = self.props.settings:onUpdate(function()
|
||||||
|
-- Trigger a dummy state update to update the settings consumer.
|
||||||
|
self:setState({})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function InternalConsumer:willUnmount()
|
||||||
|
self.disconnect()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function with(callback)
|
||||||
|
return Roact.createElement(Context.Consumer, {
|
||||||
|
render = function(settings)
|
||||||
|
return Roact.createElement(InternalConsumer, {
|
||||||
|
settings = settings,
|
||||||
|
render = callback,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
StudioProvider = StudioProvider,
|
||||||
|
with = with,
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ local Roact = require(Rojo.Roact)
|
|||||||
local Config = require(Plugin.Config)
|
local Config = require(Plugin.Config)
|
||||||
local Version = require(Plugin.Version)
|
local Version = require(Plugin.Version)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local Theme = require(Plugin.Theme)
|
local Theme = require(Plugin.Components.Theme)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -18,50 +18,53 @@ function RojoFooter:init()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function RojoFooter:render()
|
function RojoFooter:render()
|
||||||
return e("Frame", {
|
return Theme.with(function(theme)
|
||||||
LayoutOrder = 3,
|
return e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 0, 32),
|
LayoutOrder = 3,
|
||||||
BackgroundColor3 = Theme.SecondaryColor,
|
Size = UDim2.new(1, 0, 0, 32),
|
||||||
BorderSizePixel = 0,
|
BackgroundColor3 = theme.Background2,
|
||||||
}, {
|
BorderSizePixel = 0,
|
||||||
Padding = e("UIPadding", {
|
ZIndex = 2,
|
||||||
PaddingTop = UDim.new(0, 4),
|
|
||||||
PaddingBottom = UDim.new(0, 4),
|
|
||||||
PaddingLeft = UDim.new(0, 8),
|
|
||||||
PaddingRight = UDim.new(0, 8),
|
|
||||||
}),
|
|
||||||
|
|
||||||
LogoContainer = e("Frame", {
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
|
|
||||||
Size = UDim2.new(0, 0, 0, 32),
|
|
||||||
}, {
|
}, {
|
||||||
Logo = e("ImageLabel", {
|
Padding = e("UIPadding", {
|
||||||
Image = Assets.Images.Logo,
|
PaddingTop = UDim.new(0, 4),
|
||||||
Size = UDim2.new(0, 80, 0, 40),
|
PaddingBottom = UDim.new(0, 4),
|
||||||
ScaleType = Enum.ScaleType.Fit,
|
PaddingLeft = UDim.new(0, 8),
|
||||||
BackgroundTransparency = 1,
|
PaddingRight = UDim.new(0, 8),
|
||||||
Position = UDim2.new(0, 0, 1, -10),
|
|
||||||
AnchorPoint = Vector2.new(0, 1),
|
|
||||||
}),
|
}),
|
||||||
}),
|
|
||||||
|
|
||||||
Version = e("TextLabel", {
|
LogoContainer = e("Frame", {
|
||||||
Position = UDim2.new(1, 0, 0, 0),
|
BackgroundTransparency = 1,
|
||||||
Size = UDim2.new(0, 0, 1, 0),
|
|
||||||
AnchorPoint = Vector2.new(1, 0),
|
|
||||||
Font = Theme.TitleFont,
|
|
||||||
TextSize = 18,
|
|
||||||
Text = Version.display(Config.version),
|
|
||||||
TextXAlignment = Enum.TextXAlignment.Right,
|
|
||||||
TextColor3 = Theme.LightTextColor,
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
|
|
||||||
[Roact.Change.AbsoluteSize] = function(rbx)
|
Size = UDim2.new(0, 0, 0, 32),
|
||||||
self.setFooterVersionSize(rbx.AbsoluteSize)
|
}, {
|
||||||
end,
|
Logo = e("ImageLabel", {
|
||||||
}),
|
Image = Assets.Images.Logo,
|
||||||
})
|
Size = UDim2.new(0, 80, 0, 40),
|
||||||
|
ScaleType = Enum.ScaleType.Fit,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Position = UDim2.new(0, 0, 1, -10),
|
||||||
|
AnchorPoint = Vector2.new(0, 1),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Version = e("TextLabel", {
|
||||||
|
Position = UDim2.new(1, 0, 0, 0),
|
||||||
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
|
AnchorPoint = Vector2.new(1, 0),
|
||||||
|
Font = theme.TitleFont,
|
||||||
|
TextSize = 18,
|
||||||
|
Text = Version.display(Config.version),
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Right,
|
||||||
|
TextColor3 = theme.Text2,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
|
[Roact.Change.AbsoluteSize] = function(rbx)
|
||||||
|
self.setFooterVersionSize(rbx.AbsoluteSize)
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return RojoFooter
|
return RojoFooter
|
||||||
119
plugin/src/Components/SettingsPanel.lua
Normal file
119
plugin/src/Components/SettingsPanel.lua
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
||||||
|
|
||||||
|
local Plugin = script:FindFirstAncestor("Plugin")
|
||||||
|
|
||||||
|
local Checkbox = require(Plugin.Components.Checkbox)
|
||||||
|
local FitList = require(Plugin.Components.FitList)
|
||||||
|
local FitText = require(Plugin.Components.FitText)
|
||||||
|
local FormButton = require(Plugin.Components.FormButton)
|
||||||
|
local Panel = require(Plugin.Components.Panel)
|
||||||
|
local PluginSettings = require(Plugin.Components.PluginSettings)
|
||||||
|
local Theme = require(Plugin.Components.Theme)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local SettingsPanel = Roact.Component:extend("SettingsPanel")
|
||||||
|
|
||||||
|
function SettingsPanel:render()
|
||||||
|
local back = self.props.back
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
return PluginSettings.with(function(settings)
|
||||||
|
return e(Panel, nil, {
|
||||||
|
Layout = Roact.createElement("UIListLayout", {
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 16),
|
||||||
|
}),
|
||||||
|
|
||||||
|
OpenScriptsExternally = e(FitList, {
|
||||||
|
containerProps = {
|
||||||
|
LayoutOrder = 1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
},
|
||||||
|
layoutProps = {
|
||||||
|
Padding = UDim.new(0, 4),
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Label = e(FitText, {
|
||||||
|
Kind = "TextLabel",
|
||||||
|
LayoutOrder = 1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = theme.MainFont,
|
||||||
|
TextSize = 16,
|
||||||
|
Text = "Open Scripts Externally",
|
||||||
|
TextColor3 = theme.Text1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Padding = e("Frame", {
|
||||||
|
Size = UDim2.new(0, 8, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
LayoutOrder = 2,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Input = e(Checkbox, {
|
||||||
|
layoutOrder = 3,
|
||||||
|
checked = settings:get("openScriptsExternally"),
|
||||||
|
onChange = function(newValue)
|
||||||
|
settings:set("openScriptsExternally", not settings:get("openScriptsExternally"))
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
TwoWaySync = e(FitList, {
|
||||||
|
containerProps = {
|
||||||
|
LayoutOrder = 2,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
},
|
||||||
|
layoutProps = {
|
||||||
|
Padding = UDim.new(0, 4),
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Label = e(FitText, {
|
||||||
|
Kind = "TextLabel",
|
||||||
|
LayoutOrder = 1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = theme.MainFont,
|
||||||
|
TextSize = 16,
|
||||||
|
Text = "Two-Way Sync (Experimental!)",
|
||||||
|
TextColor3 = theme.Text1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Padding = e("Frame", {
|
||||||
|
Size = UDim2.new(0, 8, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
LayoutOrder = 2,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Input = e(Checkbox, {
|
||||||
|
layoutOrder = 3,
|
||||||
|
checked = settings:get("twoWaySync"),
|
||||||
|
onChange = function(newValue)
|
||||||
|
settings:set("twoWaySync", not settings:get("twoWaySync"))
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
BackButton = e(FormButton, {
|
||||||
|
layoutOrder = 4,
|
||||||
|
text = "Okay",
|
||||||
|
secondary = true,
|
||||||
|
onClick = function()
|
||||||
|
back()
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return SettingsPanel
|
||||||
104
plugin/src/Components/Theme.lua
Normal file
104
plugin/src/Components/Theme.lua
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
--[[
|
||||||
|
Theming system taking advantage of Roact's new context API.
|
||||||
|
|
||||||
|
Doesn't use colors provided by Studio and instead just branches on theme
|
||||||
|
name. This isn't exactly best practice.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local Studio = settings():GetService("Studio")
|
||||||
|
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
|
||||||
|
local Roact = require(Rojo.Roact)
|
||||||
|
local Log = require(Rojo.Log)
|
||||||
|
|
||||||
|
local strict = require(script.Parent.Parent.strict)
|
||||||
|
|
||||||
|
local lightTheme = strict("Theme", {
|
||||||
|
ButtonFont = Enum.Font.GothamSemibold,
|
||||||
|
InputFont = Enum.Font.Code,
|
||||||
|
TitleFont = Enum.Font.GothamBold,
|
||||||
|
MainFont = Enum.Font.Gotham,
|
||||||
|
|
||||||
|
Brand1 = Color3.fromRGB(225, 56, 53),
|
||||||
|
|
||||||
|
Text1 = Color3.fromRGB(64, 64, 64),
|
||||||
|
Text2 = Color3.fromRGB(160, 160, 160),
|
||||||
|
TextOnAccent = Color3.fromRGB(235, 235, 235),
|
||||||
|
|
||||||
|
Background1 = Color3.fromRGB(255, 255, 255),
|
||||||
|
Background2 = Color3.fromRGB(235, 235, 235),
|
||||||
|
})
|
||||||
|
|
||||||
|
local darkTheme = strict("Theme", {
|
||||||
|
ButtonFont = Enum.Font.GothamSemibold,
|
||||||
|
InputFont = Enum.Font.Code,
|
||||||
|
TitleFont = Enum.Font.GothamBold,
|
||||||
|
MainFont = Enum.Font.Gotham,
|
||||||
|
|
||||||
|
Brand1 = Color3.fromRGB(225, 56, 53),
|
||||||
|
|
||||||
|
Text1 = Color3.fromRGB(235, 235, 235),
|
||||||
|
Text2 = Color3.fromRGB(200, 200, 200),
|
||||||
|
TextOnAccent = Color3.fromRGB(235, 235, 235),
|
||||||
|
|
||||||
|
Background1 = Color3.fromRGB(48, 48, 48),
|
||||||
|
Background2 = Color3.fromRGB(64, 64, 64),
|
||||||
|
})
|
||||||
|
|
||||||
|
local Context = Roact.createContext(lightTheme)
|
||||||
|
|
||||||
|
local StudioProvider = Roact.Component:extend("StudioProvider")
|
||||||
|
|
||||||
|
-- Pull the current theme from Roblox Studio and update state with it.
|
||||||
|
function StudioProvider:updateTheme()
|
||||||
|
local studioTheme = Studio.Theme
|
||||||
|
|
||||||
|
if studioTheme.Name == "Light" then
|
||||||
|
self:setState({
|
||||||
|
theme = lightTheme,
|
||||||
|
})
|
||||||
|
elseif studioTheme.Name == "Dark" then
|
||||||
|
self:setState({
|
||||||
|
theme = darkTheme,
|
||||||
|
})
|
||||||
|
else
|
||||||
|
Log.warn("Unexpected theme '{}'' -- falling back to light theme!", studioTheme.Name)
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
theme = lightTheme,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function StudioProvider:init()
|
||||||
|
self:updateTheme()
|
||||||
|
end
|
||||||
|
|
||||||
|
function StudioProvider:render()
|
||||||
|
return Roact.createElement(Context.Provider, {
|
||||||
|
value = self.state.theme,
|
||||||
|
}, self.props[Roact.Children])
|
||||||
|
end
|
||||||
|
|
||||||
|
function StudioProvider:didMount()
|
||||||
|
self.connection = Studio.ThemeChanged:Connect(function()
|
||||||
|
self:updateTheme()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function StudioProvider:willUnmount()
|
||||||
|
self.connection:Disconnect()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function with(callback)
|
||||||
|
return Roact.createElement(Context.Consumer, {
|
||||||
|
render = callback,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
StudioProvider = StudioProvider,
|
||||||
|
Consumer = Context.Consumer,
|
||||||
|
with = with,
|
||||||
|
}
|
||||||
@@ -5,8 +5,8 @@ local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
|
|||||||
return strict("Config", {
|
return strict("Config", {
|
||||||
isDevBuild = isDevBuild,
|
isDevBuild = isDevBuild,
|
||||||
codename = "Epiphany",
|
codename = "Epiphany",
|
||||||
version = {0, 6, 0, "-alpha.2"},
|
version = {6, 0, 0, "-rc.1"},
|
||||||
expectedServerVersionString = "0.6.0 or newer",
|
expectedServerVersionString = "6.0 or newer",
|
||||||
protocolVersion = 3,
|
protocolVersion = 3,
|
||||||
defaultHost = "localhost",
|
defaultHost = "localhost",
|
||||||
defaultPort = 34872,
|
defaultPort = 34872,
|
||||||
|
|||||||
@@ -25,14 +25,6 @@ local VALUES = {
|
|||||||
[Environment.Test] = true,
|
[Environment.Test] = true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
UnstableTwoWaySync = {
|
|
||||||
type = "BoolValue",
|
|
||||||
values = {
|
|
||||||
[Environment.User] = false,
|
|
||||||
[Environment.Dev] = false,
|
|
||||||
[Environment.Test] = false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local CONTAINER_NAME = "RojoDevSettings" .. Config.codename
|
local CONTAINER_NAME = "RojoDevSettings" .. Config.codename
|
||||||
@@ -140,10 +132,6 @@ function DevSettings:shouldTypecheck()
|
|||||||
return getValue("TypecheckingEnabled")
|
return getValue("TypecheckingEnabled")
|
||||||
end
|
end
|
||||||
|
|
||||||
function DevSettings:twoWaySyncEnabled()
|
|
||||||
return getValue("UnstableTwoWaySync")
|
|
||||||
end
|
|
||||||
|
|
||||||
function _G.ROJO_DEV_CREATE()
|
function _G.ROJO_DEV_CREATE()
|
||||||
DevSettings:createDevSettings()
|
DevSettings:createDevSettings()
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,9 +11,22 @@ InstanceMap.__index = InstanceMap
|
|||||||
|
|
||||||
function InstanceMap.new(onInstanceChanged)
|
function InstanceMap.new(onInstanceChanged)
|
||||||
local self = {
|
local self = {
|
||||||
|
-- A map from IDs to instances.
|
||||||
fromIds = {},
|
fromIds = {},
|
||||||
|
|
||||||
|
-- A map from instances to IDs.
|
||||||
fromInstances = {},
|
fromInstances = {},
|
||||||
|
|
||||||
|
-- A set of all instances that updates should be paused for. This set
|
||||||
|
-- should generally be empty, and will be filled by pauseInstance
|
||||||
|
-- temporarily.
|
||||||
|
pausedUpdateInstances = {},
|
||||||
|
|
||||||
|
-- A map from instances to a signal or list of signals connected to it.
|
||||||
instancesToSignal = {},
|
instancesToSignal = {},
|
||||||
|
|
||||||
|
-- Callback that's invoked whenever an instance is changed and it was
|
||||||
|
-- not paused.
|
||||||
onInstanceChanged = onInstanceChanged,
|
onInstanceChanged = onInstanceChanged,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +131,32 @@ function InstanceMap:destroyId(id)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Pause updates for an instance momentarily and invoke a callback.
|
||||||
|
|
||||||
|
If the callback throws an error, InstanceMap will still be kept in a
|
||||||
|
consistent state.
|
||||||
|
]]
|
||||||
|
function InstanceMap:pauseInstance(instance, callback)
|
||||||
|
local id = self.fromInstances[instance]
|
||||||
|
|
||||||
|
-- If we don't know about this instance, ignore it and do not invoke the
|
||||||
|
-- callback.
|
||||||
|
if id == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
self.pausedUpdateInstances[instance] = true
|
||||||
|
local success, result = xpcall(callback, debug.traceback)
|
||||||
|
self.pausedUpdateInstances[instance] = false
|
||||||
|
|
||||||
|
if success then
|
||||||
|
return result
|
||||||
|
else
|
||||||
|
error(result, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function InstanceMap:__connectSignals(instance)
|
function InstanceMap:__connectSignals(instance)
|
||||||
-- ValueBase instances have an overriden version of the Changed signal that
|
-- ValueBase instances have an overriden version of the Changed signal that
|
||||||
-- only detects changes to their Value property.
|
-- only detects changes to their Value property.
|
||||||
@@ -150,9 +189,15 @@ end
|
|||||||
function InstanceMap:__maybeFireInstanceChanged(instance, propertyName)
|
function InstanceMap:__maybeFireInstanceChanged(instance, propertyName)
|
||||||
Log.trace("{}.{} changed", instance:GetFullName(), propertyName)
|
Log.trace("{}.{} changed", instance:GetFullName(), propertyName)
|
||||||
|
|
||||||
if self.onInstanceChanged ~= nil then
|
if self.pausedUpdateInstances[instance] then
|
||||||
self.onInstanceChanged(instance, propertyName)
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if self.onInstanceChanged == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
self.onInstanceChanged(instance, propertyName)
|
||||||
end
|
end
|
||||||
|
|
||||||
function InstanceMap:__disconnectSignals(instance)
|
function InstanceMap:__disconnectSignals(instance)
|
||||||
|
|||||||
25
plugin/src/PatchSet.lua
Normal file
25
plugin/src/PatchSet.lua
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
--[[
|
||||||
|
Methods to operate on either a patch created by the hydrate method, or a
|
||||||
|
patch returned from the API.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local t = require(script.Parent.Parent.t)
|
||||||
|
|
||||||
|
local Types = require(script.Parent.Types)
|
||||||
|
|
||||||
|
local PatchSet = {}
|
||||||
|
|
||||||
|
PatchSet.validate = t.interface({
|
||||||
|
removed = t.array(t.union(Types.RbxId, t.Instance)),
|
||||||
|
added = t.map(Types.RbxId, Types.ApiInstance),
|
||||||
|
updated = t.array(Types.ApiInstanceUpdate),
|
||||||
|
})
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Invert the given PatchSet using the given instance map.
|
||||||
|
]]
|
||||||
|
function PatchSet.invert(patchSet, instanceMap)
|
||||||
|
error("not yet implemented", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return PatchSet
|
||||||
@@ -5,24 +5,12 @@
|
|||||||
|
|
||||||
local RbxDom = require(script.Parent.Parent.RbxDom)
|
local RbxDom = require(script.Parent.Parent.RbxDom)
|
||||||
local t = require(script.Parent.Parent.t)
|
local t = require(script.Parent.Parent.t)
|
||||||
local Log = require(script.Parent.Parent.Log)
|
|
||||||
|
|
||||||
local Types = require(script.Parent.Types)
|
local Types = require(script.Parent.Types)
|
||||||
local invariant = require(script.Parent.invariant)
|
local invariant = require(script.Parent.invariant)
|
||||||
local getCanonicalProperty = require(script.Parent.getCanonicalProperty)
|
local getCanonicalProperty = require(script.Parent.getCanonicalProperty)
|
||||||
local setCanonicalProperty = require(script.Parent.setCanonicalProperty)
|
local setCanonicalProperty = require(script.Parent.setCanonicalProperty)
|
||||||
|
local PatchSet = require(script.Parent.PatchSet)
|
||||||
--[[
|
|
||||||
This interface represents either a patch created by the hydrate method, or a
|
|
||||||
patch returned from the API.
|
|
||||||
|
|
||||||
This type should be a subset of Types.ApiInstanceUpdate.
|
|
||||||
]]
|
|
||||||
local IPatch = t.interface({
|
|
||||||
removed = t.array(t.union(Types.RbxId, t.Instance)),
|
|
||||||
added = t.map(Types.RbxId, Types.ApiInstance),
|
|
||||||
updated = t.array(Types.ApiInstanceUpdate),
|
|
||||||
})
|
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Attempt to safely set the parent of an instance.
|
Attempt to safely set the parent of an instance.
|
||||||
@@ -86,7 +74,7 @@ end
|
|||||||
editable by scripts.
|
editable by scripts.
|
||||||
]]
|
]]
|
||||||
local applyPatchSchema = Types.ifEnabled(t.tuple(
|
local applyPatchSchema = Types.ifEnabled(t.tuple(
|
||||||
IPatch
|
PatchSet.validate
|
||||||
))
|
))
|
||||||
function Reconciler:applyPatch(patch)
|
function Reconciler:applyPatch(patch)
|
||||||
assert(applyPatchSchema(patch))
|
assert(applyPatchSchema(patch))
|
||||||
@@ -287,7 +275,7 @@ local hydrateSchema = Types.ifEnabled(t.tuple(
|
|||||||
t.map(Types.RbxId, Types.VirtualInstance),
|
t.map(Types.RbxId, Types.VirtualInstance),
|
||||||
Types.RbxId,
|
Types.RbxId,
|
||||||
t.Instance,
|
t.Instance,
|
||||||
IPatch
|
PatchSet.validate
|
||||||
))
|
))
|
||||||
function Reconciler:__hydrateInternal(apiInstances, id, instance, hydratePatch)
|
function Reconciler:__hydrateInternal(apiInstances, id, instance, hydratePatch)
|
||||||
assert(hydrateSchema(apiInstances, id, instance, hydratePatch))
|
assert(hydrateSchema(apiInstances, id, instance, hydratePatch))
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
local StudioService = game:GetService("StudioService")
|
||||||
|
|
||||||
local Log = require(script.Parent.Parent.Log)
|
local Log = require(script.Parent.Parent.Log)
|
||||||
local Fmt = require(script.Parent.Parent.Fmt)
|
local Fmt = require(script.Parent.Parent.Fmt)
|
||||||
local t = require(script.Parent.Parent.t)
|
local t = require(script.Parent.Parent.t)
|
||||||
|
|
||||||
local DevSettings = require(script.Parent.DevSettings)
|
|
||||||
local InstanceMap = require(script.Parent.InstanceMap)
|
local InstanceMap = require(script.Parent.InstanceMap)
|
||||||
local Reconciler = require(script.Parent.Reconciler)
|
local Reconciler = require(script.Parent.Reconciler)
|
||||||
local strict = require(script.Parent.strict)
|
local strict = require(script.Parent.strict)
|
||||||
@@ -43,6 +44,8 @@ ServeSession.Status = Status
|
|||||||
|
|
||||||
local validateServeOptions = t.strictInterface({
|
local validateServeOptions = t.strictInterface({
|
||||||
apiContext = t.table,
|
apiContext = t.table,
|
||||||
|
openScriptsExternally = t.boolean,
|
||||||
|
twoWaySync = t.boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
function ServeSession.new(options)
|
function ServeSession.new(options)
|
||||||
@@ -57,12 +60,28 @@ function ServeSession.new(options)
|
|||||||
local instanceMap = InstanceMap.new(onInstanceChanged)
|
local instanceMap = InstanceMap.new(onInstanceChanged)
|
||||||
local reconciler = Reconciler.new(instanceMap)
|
local reconciler = Reconciler.new(instanceMap)
|
||||||
|
|
||||||
|
local connections = {}
|
||||||
|
|
||||||
|
local connection = StudioService
|
||||||
|
:GetPropertyChangedSignal("ActiveScript")
|
||||||
|
:Connect(function()
|
||||||
|
local activeScript = StudioService.ActiveScript
|
||||||
|
|
||||||
|
if activeScript ~= nil then
|
||||||
|
self:__onActiveScriptChanged(activeScript)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
table.insert(connections, connection)
|
||||||
|
|
||||||
self = {
|
self = {
|
||||||
__status = Status.NotStarted,
|
__status = Status.NotStarted,
|
||||||
__apiContext = options.apiContext,
|
__apiContext = options.apiContext,
|
||||||
|
__openScriptsExternally = options.openScriptsExternally,
|
||||||
|
__twoWaySync = options.twoWaySync,
|
||||||
__reconciler = reconciler,
|
__reconciler = reconciler,
|
||||||
__instanceMap = instanceMap,
|
__instanceMap = instanceMap,
|
||||||
__statusChangedCallback = nil,
|
__statusChangedCallback = nil,
|
||||||
|
__connections = connections,
|
||||||
}
|
}
|
||||||
|
|
||||||
setmetatable(self, ServeSession)
|
setmetatable(self, ServeSession)
|
||||||
@@ -108,8 +127,39 @@ function ServeSession:stop()
|
|||||||
self:__stopInternal()
|
self:__stopInternal()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function ServeSession:__onActiveScriptChanged(activeScript)
|
||||||
|
if not self.__openScriptsExternally then
|
||||||
|
Log.trace("Not opening script {} because feature not enabled.", activeScript)
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.__status ~= Status.Connected then
|
||||||
|
Log.trace("Not opening script {} because session is not connected.", activeScript)
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local scriptId = self.__instanceMap.fromInstances[activeScript]
|
||||||
|
if scriptId == nil then
|
||||||
|
Log.trace("Not opening script {} because it is not known by Rojo.", activeScript)
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
Log.debug("Trying to open script {} externally...", activeScript)
|
||||||
|
|
||||||
|
-- Force-close the script inside Studio
|
||||||
|
local existingParent = activeScript.Parent
|
||||||
|
activeScript.Parent = nil
|
||||||
|
activeScript.Parent = existingParent
|
||||||
|
|
||||||
|
-- Notify the Rojo server to open this script
|
||||||
|
self.__apiContext:open(scriptId)
|
||||||
|
end
|
||||||
|
|
||||||
function ServeSession:__onInstanceChanged(instance, propertyName)
|
function ServeSession:__onInstanceChanged(instance, propertyName)
|
||||||
if not DevSettings:twoWaySyncEnabled() then
|
if not self.__twoWaySync then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -200,6 +250,11 @@ function ServeSession:__stopInternal(err)
|
|||||||
self:__setStatus(Status.Disconnected, err)
|
self:__setStatus(Status.Disconnected, err)
|
||||||
self.__apiContext:disconnect()
|
self.__apiContext:disconnect()
|
||||||
self.__instanceMap:stop()
|
self.__instanceMap:stop()
|
||||||
|
|
||||||
|
for _, connection in ipairs(self.__connections) do
|
||||||
|
connection:Disconnect()
|
||||||
|
end
|
||||||
|
self.__connections = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
function ServeSession:__setStatus(status, detail)
|
function ServeSession:__setStatus(status, detail)
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
local strict = require(script.Parent.strict)
|
|
||||||
|
|
||||||
return strict("Theme", {
|
|
||||||
ButtonFont = Enum.Font.GothamSemibold,
|
|
||||||
InputFont = Enum.Font.Code,
|
|
||||||
TitleFont = Enum.Font.GothamBold,
|
|
||||||
MainFont = Enum.Font.Gotham,
|
|
||||||
|
|
||||||
AccentColor = Color3.fromRGB(225, 56, 53),
|
|
||||||
AccentLightColor = Color3.fromRGB(255, 146, 145),
|
|
||||||
PrimaryColor = Color3.fromRGB(64, 64, 64),
|
|
||||||
SecondaryColor = Color3.fromRGB(235, 235, 235),
|
|
||||||
LightTextColor = Color3.fromRGB(160, 160, 160),
|
|
||||||
})
|
|
||||||
53
plugin/src/createSignal.lua
Normal file
53
plugin/src/createSignal.lua
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
--[[
|
||||||
|
Create a new signal that can be connected to, disconnected from, and fired.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
local signal = createSignal()
|
||||||
|
local disconnect = signal:connect(function(...)
|
||||||
|
print("fired:", ...)
|
||||||
|
end)
|
||||||
|
|
||||||
|
signal:fire("a", "b", "c")
|
||||||
|
disconnect()
|
||||||
|
|
||||||
|
Avoids mutating listeners list directly to prevent iterator invalidation if
|
||||||
|
a listener is disconnected while the signal is firing.
|
||||||
|
]]
|
||||||
|
local function createSignal()
|
||||||
|
local listeners = {}
|
||||||
|
|
||||||
|
local function connect(newListener)
|
||||||
|
local nextListeners = {}
|
||||||
|
for listener in pairs(listeners) do
|
||||||
|
nextListeners[listener] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
nextListeners[newListener] = true
|
||||||
|
listeners = nextListeners
|
||||||
|
|
||||||
|
return function()
|
||||||
|
local nextListeners = {}
|
||||||
|
for listener in pairs(listeners) do
|
||||||
|
if listener ~= newListener then
|
||||||
|
nextListeners[listener] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
listeners = nextListeners
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fire(...)
|
||||||
|
for listener in pairs(listeners) do
|
||||||
|
listener(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
connect = connect,
|
||||||
|
fire = fire,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return createSignal
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
local RbxDom = require(script.Parent.Parent.RbxDom)
|
local RbxDom = require(script.Parent.Parent.RbxDom)
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Attempts to set a property on the given instance.
|
Attempts to read a property from the given instance.
|
||||||
]]
|
]]
|
||||||
local function getCanonincalProperty(instance, propertyName)
|
local function getCanonincalProperty(instance, propertyName)
|
||||||
local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
|
local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
|
||||||
|
|||||||
@@ -14,12 +14,20 @@ local Roact = require(script.Parent.Roact)
|
|||||||
|
|
||||||
local Config = require(script.Config)
|
local Config = require(script.Config)
|
||||||
local App = require(script.Components.App)
|
local App = require(script.Components.App)
|
||||||
|
local Theme = require(script.Components.Theme)
|
||||||
|
local PluginSettings = require(script.Components.PluginSettings)
|
||||||
|
|
||||||
local app = Roact.createElement(App, {
|
local app = Roact.createElement(Theme.StudioProvider, nil, {
|
||||||
plugin = plugin,
|
Roact.createElement(PluginSettings.StudioProvider, {
|
||||||
|
plugin = plugin,
|
||||||
|
}, {
|
||||||
|
RojoUI = Roact.createElement(App, {
|
||||||
|
plugin = plugin,
|
||||||
|
}),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
local tree = Roact.mount(app, game:GetService("CoreGui"), "Rojo UI")
|
local tree = Roact.mount(app, nil, "Rojo UI")
|
||||||
|
|
||||||
plugin.Unloading:Connect(function()
|
plugin.Unloading:Connect(function()
|
||||||
Roact.unmount(tree)
|
Roact.unmount(tree)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ default = []
|
|||||||
unstable_glob_ignore_paths = []
|
unstable_glob_ignore_paths = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
env_logger = "0.6.2"
|
env_logger = "0.7.1"
|
||||||
insta = { version = "0.13.1", features = ["redactions"] }
|
insta = { version = "0.13.1", features = ["redactions"] }
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
paste = "0.1.5"
|
paste = "0.1.5"
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: rojo-test/src/build_test.rs
|
||||||
|
expression: contents
|
||||||
|
---
|
||||||
|
<roblox version="4">
|
||||||
|
<Item class="DataModel" referent="0">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">infer-service-name</string>
|
||||||
|
</Properties>
|
||||||
|
<Item class="HttpService" referent="1">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">HttpService</string>
|
||||||
|
<bool name="HttpEnabled">true</bool>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
<Item class="ReplicatedStorage" referent="2">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">ReplicatedStorage</string>
|
||||||
|
</Properties>
|
||||||
|
<Item class="ModuleScript" referent="3">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">Main</string>
|
||||||
|
<string name="Source">-- hello, from main</string>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</roblox>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
source: rojo-test/src/build_test.rs
|
||||||
|
expression: contents
|
||||||
|
---
|
||||||
|
<roblox version="4">
|
||||||
|
<Item class="DataModel" referent="0">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">infer-service-name</string>
|
||||||
|
</Properties>
|
||||||
|
<Item class="StarterPlayer" referent="1">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">StarterPlayer</string>
|
||||||
|
</Properties>
|
||||||
|
<Item class="StarterCharacterScripts" referent="2">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">StarterCharacterScripts</string>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
<Item class="StarterPlayerScripts" referent="3">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">StarterPlayerScripts</string>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</roblox>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
source: rojo-test/src/build_test.rs
|
||||||
|
expression: contents
|
||||||
|
---
|
||||||
|
<roblox version="4">
|
||||||
|
<Item class="ModuleScript" referent="0">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">json_as_lua</string>
|
||||||
|
<string name="Source">return {
|
||||||
|
["1invalidident"] = "nice",
|
||||||
|
array = {1, 2, 3},
|
||||||
|
["false"] = false,
|
||||||
|
float = 1234.5452,
|
||||||
|
int = 1234,
|
||||||
|
null = nil,
|
||||||
|
object = {
|
||||||
|
hello = "world",
|
||||||
|
},
|
||||||
|
["true"] = true,
|
||||||
|
}</string>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
</roblox>
|
||||||
6
rojo-test/build-tests/deep_nesting/default.project.json
Normal file
6
rojo-test/build-tests/deep_nesting/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "deep_nesting",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "infer-service-name",
|
||||||
|
"tree": {
|
||||||
|
"$className": "DataModel",
|
||||||
|
"ReplicatedStorage": {
|
||||||
|
"Main": {
|
||||||
|
"$path": "main.lua"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HttpService": {
|
||||||
|
"$properties": {
|
||||||
|
"HttpEnabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
rojo-test/build-tests/infer_service_name/main.lua
Normal file
1
rojo-test/build-tests/infer_service_name/main.lua
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-- hello, from main
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "infer-service-name",
|
||||||
|
"tree": {
|
||||||
|
"$className": "DataModel",
|
||||||
|
|
||||||
|
"StarterPlayer": {
|
||||||
|
"StarterPlayerScripts": {},
|
||||||
|
"StarterCharacterScripts": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
rojo-test/build-tests/infer_starter_player/main.lua
Normal file
1
rojo-test/build-tests/infer_starter_player/main.lua
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-- hello, from main
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "init_with_children",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
rojo-test/build-tests/json_as_lua/default.project.json
Normal file
6
rojo-test/build-tests/json_as_lua/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "json_as_lua",
|
||||||
|
"tree": {
|
||||||
|
"$path": "make-me-a-script.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
rojo-test/build-tests/json_as_lua/make-me-a-script.json
Normal file
12
rojo-test/build-tests/json_as_lua/make-me-a-script.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"array": [1, 2, 3],
|
||||||
|
"object": {
|
||||||
|
"hello": "world"
|
||||||
|
},
|
||||||
|
"true": true,
|
||||||
|
"false": false,
|
||||||
|
"null": null,
|
||||||
|
"int": 1234,
|
||||||
|
"float": 1234.5452,
|
||||||
|
"1invalidident": "nice"
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
This is a bare text file with no project.
|
|
||||||
6
rojo-test/build-tests/rbxmx_ref/default.project.json
Normal file
6
rojo-test/build-tests/rbxmx_ref/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "rbxmx_ref",
|
||||||
|
"tree": {
|
||||||
|
"$path": "model.rbxmx"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
rojo-test/serve-tests/add_folder/default.project.json
Normal file
6
rojo-test/serve-tests/add_folder/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "add_folder",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
rojo-test/serve-tests/edit_init/default.project.json
Normal file
6
rojo-test/serve-tests/edit_init/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "edit_init",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
Hello, world!
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "move_folder_of_stuff",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
rojo-test/serve-tests/remove_file/default.project.json
Normal file
6
rojo-test/serve-tests/remove_file/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "remove_file",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
rojo-test/serve-tests/scripts/default.project.json
Normal file
6
rojo-test/serve-tests/scripts/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "scripts",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user