forked from rojo-rbx/rojo
Compare commits
55 Commits
aarch-wind
...
v7.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
825726c883 | ||
|
|
54e63d88d4 | ||
|
|
4018c97cb6 | ||
|
|
d0b029f995 | ||
|
|
aabe6d11b2 | ||
|
|
181cc37744 | ||
|
|
cd78f5c02c | ||
|
|
441c469966 | ||
|
|
f3c423d77d | ||
|
|
beb497878b | ||
|
|
6ea95d487c | ||
|
|
80a381dbb1 | ||
|
|
59e36491a5 | ||
|
|
c1326ba06e | ||
|
|
e2633126ee | ||
|
|
5f33435f3c | ||
|
|
54e0ff230b | ||
|
|
4e9e6233ff | ||
|
|
0056849b51 | ||
|
|
2ddb21ec5f | ||
|
|
a4eb65ca3f | ||
|
|
3002d250a1 | ||
|
|
9598553e5d | ||
|
|
7f68d9887b | ||
|
|
e092a7301f | ||
|
|
6dfdfbe514 | ||
|
|
7860f2717f | ||
|
|
60f19df9a0 | ||
|
|
951f0cda0b | ||
|
|
227042d6b1 | ||
|
|
b2c4f550ee | ||
|
|
4ddbefa88f | ||
|
|
d935115591 | ||
|
|
bd2ea42732 | ||
|
|
3bac38ee34 | ||
|
|
a7a4f6d8f2 | ||
|
|
80b6facbd3 | ||
|
|
7dee898400 | ||
|
|
4c4b2dbe17 | ||
|
|
73ed5ae697 | ||
|
|
833320de64 | ||
|
|
0d6ff8ef8a | ||
|
|
55a207a275 | ||
|
|
f33d1f1cc4 | ||
|
|
19ca2b12fc | ||
|
|
b7d3394464 | ||
|
|
8c33100d7a | ||
|
|
80c406f196 | ||
|
|
bc2c76e5e2 | ||
|
|
4a7bddbc09 | ||
|
|
e316fdbaef | ||
|
|
34106f470f | ||
|
|
d9ab0e7de8 | ||
|
|
5ca1573e2e | ||
|
|
c9ce996626 |
2
.dir-locals.el
Normal file
2
.dir-locals.el
Normal file
@@ -0,0 +1,2 @@
|
||||
((nil . ((eglot-luau-rojo-project-path . "plugin.project.json")
|
||||
(eglot-luau-rojo-sourcemap-enabled . 't))))
|
||||
6
.github/workflows/changelog.yml
vendored
6
.github/workflows/changelog.yml
vendored
@@ -11,12 +11,12 @@ jobs:
|
||||
name: Check Actions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Changelog check
|
||||
uses: Zomzog/changelog-checker@v1.3.0
|
||||
with:
|
||||
fileName: CHANGELOG.md
|
||||
fileName: CHANGELOG.md
|
||||
noChangelogLabel: skip changelog
|
||||
checkNotification: Simple
|
||||
env:
|
||||
|
||||
74
.github/workflows/ci.yml
vendored
74
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
os: [ubuntu-22.04, windows-latest, macos-latest, windows-11-arm, ubuntu-22.04-arm]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -26,13 +26,14 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Setup Aftman
|
||||
uses: ok-nick/setup-aftman@v0.3.0
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
version: 'v0.2.7'
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Build
|
||||
run: cargo build --locked --verbose
|
||||
@@ -40,6 +41,15 @@ jobs:
|
||||
- name: Test
|
||||
run: cargo test --locked --verbose
|
||||
|
||||
- name: Save Rust Cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
msrv:
|
||||
name: Check MSRV
|
||||
runs-on: ubuntu-latest
|
||||
@@ -50,19 +60,29 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.70.0
|
||||
uses: dtolnay/rust-toolchain@1.88.0
|
||||
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Setup Aftman
|
||||
uses: ok-nick/setup-aftman@v0.3.0
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
version: 'v0.2.7'
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Build
|
||||
run: cargo build --locked --verbose
|
||||
|
||||
- name: Save Rust Cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
lint:
|
||||
name: Rustfmt, Clippy, Stylua, & Selene
|
||||
runs-on: ubuntu-latest
|
||||
@@ -77,13 +97,19 @@ jobs:
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Setup Aftman
|
||||
uses: ok-nick/setup-aftman@v0.3.0
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
version: 'v0.2.7'
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Setup Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
with:
|
||||
version: 'v1.1.0'
|
||||
|
||||
- name: Stylua
|
||||
run: stylua --check plugin/src
|
||||
@@ -97,3 +123,11 @@ jobs:
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
|
||||
- name: Save Rust Cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
45
.github/workflows/release.yml
vendored
45
.github/workflows/release.yml
vendored
@@ -25,15 +25,13 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup Aftman
|
||||
uses: ok-nick/setup-aftman@v0.1.0
|
||||
- name: Setup Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
trust-check: false
|
||||
version: 'v0.2.6'
|
||||
version: 'v1.1.0'
|
||||
|
||||
- name: Build Plugin
|
||||
run: rojo build plugin --output Rojo.rbxm
|
||||
run: rojo build plugin.project.json --output Rojo.rbxm
|
||||
|
||||
- name: Upload Plugin to Release
|
||||
env:
|
||||
@@ -55,12 +53,12 @@ jobs:
|
||||
# https://doc.rust-lang.org/rustc/platform-support.html
|
||||
include:
|
||||
- host: linux
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
label: linux-x86_64
|
||||
|
||||
- host: linux
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-22.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
label: linux-aarch64
|
||||
|
||||
@@ -70,7 +68,7 @@ jobs:
|
||||
label: windows-x86_64
|
||||
|
||||
- host: windows
|
||||
os: windows-latest
|
||||
os: windows-11-arm
|
||||
target: aarch64-pc-windows-msvc
|
||||
label: windows-aarch64
|
||||
|
||||
@@ -98,19 +96,26 @@ jobs:
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Setup Aftman
|
||||
uses: ok-nick/setup-aftman@v0.1.0
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
trust-check: false
|
||||
version: 'v0.2.6'
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Build Release
|
||||
run: cargo build --release --locked --verbose --target ${{ matrix.target }}
|
||||
env:
|
||||
# Build into a known directory so we can find our build artifact more
|
||||
# easily.
|
||||
CARGO_TARGET_DIR: output
|
||||
|
||||
- name: Save Rust Cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Generate Artifact Name
|
||||
shell: bash
|
||||
@@ -127,11 +132,11 @@ jobs:
|
||||
mkdir staging
|
||||
|
||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||
cp "output/${{ matrix.target }}/release/$BIN.exe" staging/
|
||||
cp "target/${{ matrix.target }}/release/$BIN.exe" staging/
|
||||
cd staging
|
||||
7z a ../$ARTIFACT_NAME *
|
||||
else
|
||||
cp "output/${{ matrix.target }}/release/$BIN" staging/
|
||||
cp "target/${{ matrix.target }}/release/$BIN" staging/
|
||||
cd staging
|
||||
zip ../$ARTIFACT_NAME *
|
||||
fi
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -10,8 +10,8 @@
|
||||
/*.rbxl
|
||||
/*.rbxlx
|
||||
|
||||
# Test places for the Roblox Studio Plugin
|
||||
/plugin/*.rbxlx
|
||||
# Sourcemap for the Rojo plugin (for better intellisense)
|
||||
/sourcemap.json
|
||||
|
||||
# Roblox Studio holds 'lock' files on places
|
||||
*.rbxl.lock
|
||||
@@ -19,3 +19,7 @@
|
||||
|
||||
# Snapshot files from the 'insta' Rust crate
|
||||
**/*.snap.new
|
||||
|
||||
# Macos file system junk
|
||||
._*
|
||||
.DS_STORE
|
||||
|
||||
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"JohnnyMorganz.luau-lsp",
|
||||
"JohnnyMorganz.stylua",
|
||||
"Kampfkarren.selene-vscode",
|
||||
"rust-lang.rust-analyzer"
|
||||
]
|
||||
}
|
||||
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"luau-lsp.sourcemap.rojoProjectFile": "plugin.project.json",
|
||||
"luau-lsp.sourcemap.autogenerate": true
|
||||
}
|
||||
1004
CHANGELOG.md
1004
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -15,12 +15,29 @@ You'll want these tools to work on Rojo:
|
||||
|
||||
* Latest stable Rust compiler
|
||||
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
|
||||
* [Foreman](https://github.com/Roblox/foreman)
|
||||
* [Rokit](https://github.com/rojo-rbx/rokit)
|
||||
* [Luau Language Server](https://github.com/JohnnyMorganz/luau-lsp) (Only needed if working on the Studio plugin.)
|
||||
|
||||
When working on the Studio plugin, we recommend using this command to automatically rebuild the plugin when you save a change:
|
||||
|
||||
*(Make sure you've enabled the Studio setting to reload plugins on file change!)*
|
||||
|
||||
```bash
|
||||
bash scripts/watch-build-plugin.sh
|
||||
```
|
||||
|
||||
You can also run the plugin's unit tests with the following:
|
||||
|
||||
*(Make sure you have `run-in-roblox` installed first!)*
|
||||
|
||||
```bash
|
||||
bash scripts/unit-test-plugin.sh
|
||||
```
|
||||
|
||||
## Documentation
|
||||
Documentation impacts way more people than the individual lines of code we write.
|
||||
|
||||
If you find any problems in documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them.
|
||||
If you find any problems in the documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them.
|
||||
|
||||
## Bug Reports and Feature Requests
|
||||
Most of the tools around Rojo try to be clear when an issue is a bug. Even if they aren't, sometimes things don't work quite right.
|
||||
|
||||
378
Cargo.lock
generated
378
Cargo.lock
generated
@@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
@@ -17,6 +17,19 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.2"
|
||||
@@ -32,6 +45,12 @@ version = "1.0.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
|
||||
|
||||
[[package]]
|
||||
name = "arraydeque"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.7"
|
||||
@@ -171,6 +190,10 @@ name = "cc"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@@ -220,7 +243,7 @@ checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
@@ -378,6 +401,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
@@ -401,7 +430,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"dirs-sys",
|
||||
"dirs-sys 0.3.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -415,6 +453,18 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.10.0"
|
||||
@@ -492,7 +542,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.4.1",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -502,6 +552,12 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
@@ -609,9 +665,9 @@ version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -734,6 +790,24 @@ version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
@@ -935,6 +1009,15 @@ version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jod-thread"
|
||||
version = "0.1.2"
|
||||
@@ -950,6 +1033,15 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonc-parser"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ec4ac49f13c7b00f435f8a5bb55d725705e2cf620df35a5859321595102eb7e"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
@@ -962,9 +1054,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
@@ -986,7 +1078,7 @@ checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1001,6 +1093,16 @@ version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.21"
|
||||
@@ -1021,23 +1123,12 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lz4"
|
||||
version = "1.24.0"
|
||||
name = "lz4_flex"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e9e2dd86df36ce760a60f6ff6ad526f7ba1f14ba0356f8254fb6905e6494df1"
|
||||
checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"lz4-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lz4-sys"
|
||||
version = "1.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"twox-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1229,6 +1320,12 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.6.1"
|
||||
@@ -1241,6 +1338,29 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall 0.5.10",
|
||||
"smallvec",
|
||||
"windows-targets 0.52.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.14"
|
||||
@@ -1282,9 +1402,9 @@ checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1310,6 +1430,12 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.5"
|
||||
@@ -1361,7 +1487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
@@ -1373,7 +1499,7 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"version_check",
|
||||
]
|
||||
@@ -1401,9 +1527,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.78"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1425,7 +1551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
|
||||
dependencies = [
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1443,7 +1569,7 @@ version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1498,34 +1624,38 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rbx_binary"
|
||||
version = "0.7.7"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b85057e8ff75a1ce99248200c4b3c7b481a3d52f921f1053ecd67921dcc7930"
|
||||
checksum = "0d419f67c8012bf83569086e1208c541478b3b8e4f523deaa0b80d723fb5ef22"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"log",
|
||||
"lz4",
|
||||
"lz4_flex",
|
||||
"profiling",
|
||||
"rbx_dom_weak",
|
||||
"rbx_reflection",
|
||||
"rbx_reflection_database",
|
||||
"thiserror",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rbx_dom_weak"
|
||||
version = "2.9.0"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcd2a17d09e46af0805f8b311a926402172b97e8d9388745c9adf8f448901841"
|
||||
checksum = "bc74878a4a801afc8014b14ede4b38015a13de5d29ab0095d5ed284a744253f6"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"rbx_types",
|
||||
"serde",
|
||||
"ustr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rbx_reflection"
|
||||
version = "4.7.0"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8118ac6021d700e8debe324af6b40ecfd2cef270a00247849dbdfeebb0802677"
|
||||
checksum = "565dd3430991f35443fa6d23cc239fade2110c5089deb6bae5de77c400df4fd2"
|
||||
dependencies = [
|
||||
"rbx_types",
|
||||
"serde",
|
||||
@@ -1534,11 +1664,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rbx_reflection_database"
|
||||
version = "0.2.12+roblox-638"
|
||||
version = "2.0.1+roblox-697"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e29381d675420e841f8c02db5755cbb2545ed3e13f56c539546dc58702b512a"
|
||||
checksum = "d69035a14b103c5a9b8bc6a61d30f4ee6f2608afdee137dae09b26037dba5dc8"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"dirs 5.0.1",
|
||||
"log",
|
||||
"rbx_reflection",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
@@ -1546,9 +1677,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rbx_types"
|
||||
version = "1.10.0"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e30f49b2a3bb667e4074ba73c2dfb8ca0873f610b448ccf318a240acfdec6c73"
|
||||
checksum = "03220ffce2bd06ad04f77a003cb807f2e5b2a18e97623066a5ac735a978398af"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"bitflags 1.3.2",
|
||||
@@ -1561,10 +1692,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rbx_xml"
|
||||
version = "0.13.5"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b14b3027bc9ccd82e2fc854c8bcd25ed58318e570c355bf2cf63df9cdbd5ba8"
|
||||
checksum = "be6c302cefe9c92ed09bcbb075cd24379271de135b0af331409a64c2ea3646ee"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"base64 0.13.1",
|
||||
"log",
|
||||
"rbx_dom_weak",
|
||||
@@ -1582,6 +1714,15 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.4"
|
||||
@@ -1745,14 +1886,14 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "743bb8c693a387f1ae8d2026d82d8b0c175cc4777b97c1f7b12fdb3be595bb13"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"dirs 2.0.2",
|
||||
"thiserror",
|
||||
"winreg 0.6.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rojo"
|
||||
version = "7.4.0"
|
||||
version = "7.6.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"backtrace",
|
||||
@@ -1761,6 +1902,7 @@ dependencies = [
|
||||
"criterion",
|
||||
"crossbeam-channel",
|
||||
"csv",
|
||||
"data-encoding",
|
||||
"embed-resource",
|
||||
"env_logger",
|
||||
"fs-err",
|
||||
@@ -1770,6 +1912,7 @@ dependencies = [
|
||||
"hyper",
|
||||
"insta",
|
||||
"jod-thread",
|
||||
"jsonc-parser",
|
||||
"log",
|
||||
"maplit",
|
||||
"memofs",
|
||||
@@ -1800,6 +1943,7 @@ dependencies = [
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"winreg 0.10.1",
|
||||
"yaml-rust2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1896,6 +2040,12 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.1"
|
||||
@@ -1914,10 +2064,11 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.197"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
@@ -1932,25 +2083,36 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.197"
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.114"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2055,18 +2217,18 @@ version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.52"
|
||||
version = "2.0.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
|
||||
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -2149,9 +2311,9 @@ version = "1.0.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2261,9 +2423,9 @@ version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2331,6 +2493,12 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "twox-hash"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
@@ -2393,6 +2561,19 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ustr"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18b19e258aa08450f93369cf56dd78063586adf19e92a75b338a800f799a0208"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.7.0"
|
||||
@@ -2479,9 +2660,9 @@ dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -2513,9 +2694,9 @@ version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -2779,8 +2960,67 @@ dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust2"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ce2a4ff45552406d02501cea6c18d8a7e50228e7736a872951fe2fe75c91be7"
|
||||
dependencies = [
|
||||
"arraydeque",
|
||||
"encoding_rs",
|
||||
"hashlink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.15+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
26
Cargo.toml
26
Cargo.toml
@@ -1,8 +1,12 @@
|
||||
[package]
|
||||
name = "rojo"
|
||||
version = "7.4.0"
|
||||
rust-version = "1.70.0"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
version = "7.6.1"
|
||||
rust-version = "1.88"
|
||||
authors = [
|
||||
"Lucien Greathouse <me@lpghatguy.com>",
|
||||
"Micah Reid <git@dekkonot.com>",
|
||||
"Ken Loeffler <kenloef@gmail.com>",
|
||||
]
|
||||
description = "Enables professional-grade development tools for Roblox developers"
|
||||
license = "MPL-2.0"
|
||||
homepage = "https://rojo.space"
|
||||
@@ -51,11 +55,11 @@ memofs = { version = "0.3.0", path = "crates/memofs" }
|
||||
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
||||
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
||||
|
||||
rbx_binary = "0.7.7"
|
||||
rbx_dom_weak = "2.9.0"
|
||||
rbx_reflection = "4.7.0"
|
||||
rbx_reflection_database = "0.2.12"
|
||||
rbx_xml = "0.13.5"
|
||||
rbx_binary = "2.0.0"
|
||||
rbx_dom_weak = "4.0.0"
|
||||
rbx_reflection = "6.0.0"
|
||||
rbx_reflection_database = "2.0.1"
|
||||
rbx_xml = "2.0.0"
|
||||
|
||||
anyhow = "1.0.80"
|
||||
backtrace = "0.3.69"
|
||||
@@ -70,7 +74,6 @@ humantime = "2.1.0"
|
||||
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
|
||||
jod-thread = "0.1.2"
|
||||
log = "0.4.21"
|
||||
maplit = "1.0.2"
|
||||
num_cpus = "1.16.0"
|
||||
opener = "0.5.2"
|
||||
rayon = "1.9.0"
|
||||
@@ -82,7 +85,8 @@ reqwest = { version = "0.11.24", default-features = false, features = [
|
||||
ritz = "0.1.0"
|
||||
roblox_install = "1.0.0"
|
||||
serde = { version = "1.0.197", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.114"
|
||||
serde_json = "1.0.145"
|
||||
jsonc-parser = { version = "0.27.0", features = ["serde"] }
|
||||
toml = "0.5.11"
|
||||
termcolor = "1.4.1"
|
||||
thiserror = "1.0.57"
|
||||
@@ -90,6 +94,8 @@ tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread"] }
|
||||
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
||||
clap = { version = "3.2.25", features = ["derive"] }
|
||||
profiling = "1.0.15"
|
||||
yaml-rust2 = "0.10.3"
|
||||
data-encoding = "2.8.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.10.1"
|
||||
|
||||
@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
|
||||
|
||||
Pull requests are welcome!
|
||||
|
||||
Rojo supports Rust 1.70.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||
Rojo supports Rust 1.88 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||
|
||||
## License
|
||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
[tools]
|
||||
rojo = "rojo-rbx/rojo@7.3.0"
|
||||
selene = "Kampfkarren/selene@0.26.1"
|
||||
stylua = "JohnnyMorganz/stylua@0.18.2"
|
||||
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"
|
||||
@@ -17,6 +17,10 @@ html {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #e7e7e7
|
||||
}
|
||||
|
||||
img {
|
||||
max-width:100%;
|
||||
max-height:100%;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Roblox Studio lock files
|
||||
/*.rbxlx.lock
|
||||
/*.rbxl.lock
|
||||
/*.rbxl.lock
|
||||
|
||||
sourcemap.json
|
||||
@@ -2,4 +2,4 @@ return {
|
||||
hello = function()
|
||||
print("Hello world, from {project_name}!")
|
||||
end,
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,6 @@
|
||||
|
||||
# Roblox Studio lock files
|
||||
/*.rbxlx.lock
|
||||
/*.rbxl.lock
|
||||
/*.rbxl.lock
|
||||
|
||||
sourcemap.json
|
||||
@@ -0,0 +1 @@
|
||||
print("Hello world, from client!")
|
||||
@@ -0,0 +1 @@
|
||||
print("Hello world, from server!")
|
||||
3
assets/project-templates/place/src/shared/Hello.luau
Normal file
3
assets/project-templates/place/src/shared/Hello.luau
Normal file
@@ -0,0 +1,3 @@
|
||||
return function()
|
||||
print("Hello, world!")
|
||||
end
|
||||
@@ -1,3 +1,5 @@
|
||||
# Plugin model files
|
||||
/{project_name}.rbxmx
|
||||
/{project_name}.rbxm
|
||||
|
||||
sourcemap.json
|
||||
1
assets/project-templates/plugin/src/init.server.luau
Normal file
1
assets/project-templates/plugin/src/init.server.luau
Normal file
@@ -0,0 +1 @@
|
||||
print("Hello world, from plugin!")
|
||||
40
build.rs
40
build.rs
@@ -20,6 +20,10 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||
|
||||
let file_name = entry.file_name().to_str().unwrap().to_owned();
|
||||
|
||||
if file_name.starts_with(".git") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We can skip any TestEZ test files since they aren't necessary for
|
||||
// the plugin to run.
|
||||
if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") {
|
||||
@@ -41,33 +45,39 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||
|
||||
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
||||
let plugin_root = PathBuf::from(root_dir).join("plugin");
|
||||
let root_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||
let plugin_dir = root_dir.join("plugin");
|
||||
let templates_dir = root_dir.join("assets").join("project-templates");
|
||||
|
||||
let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?;
|
||||
let plugin_version =
|
||||
Version::parse(fs::read_to_string(plugin_root.join("Version.txt"))?.trim())?;
|
||||
Version::parse(fs::read_to_string(plugin_dir.join("Version.txt"))?.trim())?;
|
||||
|
||||
assert_eq!(
|
||||
our_version, plugin_version,
|
||||
"plugin version does not match Cargo version"
|
||||
);
|
||||
|
||||
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"))?,
|
||||
"Packages" => snapshot_from_fs_path(&plugin_root.join("Packages"))?,
|
||||
"Version.txt" => snapshot_from_fs_path(&plugin_root.join("Version.txt"))?,
|
||||
let template_snapshot = snapshot_from_fs_path(&templates_dir)?;
|
||||
|
||||
let plugin_snapshot = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => snapshot_from_fs_path(&root_dir.join("plugin.project.json"))?,
|
||||
"plugin" => VfsSnapshot::dir(hashmap! {
|
||||
"fmt" => snapshot_from_fs_path(&plugin_dir.join("fmt"))?,
|
||||
"http" => snapshot_from_fs_path(&plugin_dir.join("http"))?,
|
||||
"log" => snapshot_from_fs_path(&plugin_dir.join("log"))?,
|
||||
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_dir.join("rbx_dom_lua"))?,
|
||||
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?,
|
||||
"Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?,
|
||||
"Version.txt" => snapshot_from_fs_path(&plugin_dir.join("Version.txt"))?,
|
||||
}),
|
||||
});
|
||||
|
||||
let out_path = Path::new(&out_dir).join("plugin.bincode");
|
||||
let out_file = File::create(out_path)?;
|
||||
let template_file = File::create(Path::new(&out_dir).join("templates.bincode"))?;
|
||||
let plugin_file = File::create(Path::new(&out_dir).join("plugin.bincode"))?;
|
||||
|
||||
bincode::serialize_into(out_file, &snapshot)?;
|
||||
bincode::serialize_into(plugin_file, &plugin_snapshot)?;
|
||||
bincode::serialize_into(template_file, &template_snapshot)?;
|
||||
|
||||
println!("cargo:rerun-if-changed=build/windows/rojo-manifest.rc");
|
||||
println!("cargo:rerun-if-changed=build/windows/rojo.manifest");
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
name = "memofs"
|
||||
description = "Virtual filesystem with configurable backends."
|
||||
version = "0.3.0"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
authors = [
|
||||
"Lucien Greathouse <me@lpghatguy.com>",
|
||||
"Micah Reid <git@dekkonot.com>",
|
||||
"Ken Loeffler <kenloef@gmail.com>",
|
||||
]
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -228,23 +228,17 @@ impl VfsBackend for InMemoryFs {
|
||||
}
|
||||
|
||||
fn must_be_file<T>(path: &Path) -> io::Result<T> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"path {} was a directory, but must be a file",
|
||||
path.display()
|
||||
),
|
||||
))
|
||||
Err(io::Error::other(format!(
|
||||
"path {} was a directory, but must be a file",
|
||||
path.display()
|
||||
)))
|
||||
}
|
||||
|
||||
fn must_be_dir<T>(path: &Path) -> io::Result<T> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"path {} was a file, but must be a directory",
|
||||
path.display()
|
||||
),
|
||||
))
|
||||
Err(io::Error::other(format!(
|
||||
"path {} was a file, but must be a directory",
|
||||
path.display()
|
||||
)))
|
||||
}
|
||||
|
||||
fn not_found<T>(path: &Path) -> io::Result<T> {
|
||||
|
||||
@@ -15,45 +15,27 @@ impl NoopBackend {
|
||||
|
||||
impl VfsBackend for NoopBackend {
|
||||
fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
@@ -61,17 +43,11 @@ impl VfsBackend for NoopBackend {
|
||||
}
|
||||
|
||||
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn unwatch(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -109,15 +109,13 @@ impl VfsBackend for StdBackend {
|
||||
self.watches.insert(path.to_path_buf());
|
||||
self.watcher
|
||||
.watch(path, RecursiveMode::Recursive)
|
||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||
.map_err(io::Error::other)
|
||||
}
|
||||
}
|
||||
|
||||
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
|
||||
self.watches.remove(path);
|
||||
self.watcher
|
||||
.unwatch(path)
|
||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||
self.watcher.unwatch(path).map_err(io::Error::other)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,19 +5,13 @@ use serde::Serialize;
|
||||
/// Enables redacting any value that serializes as a string.
|
||||
///
|
||||
/// Used for transforming Rojo instance IDs into something deterministic.
|
||||
#[derive(Default)]
|
||||
pub struct RedactionMap {
|
||||
ids: HashMap<String, usize>,
|
||||
last_id: usize,
|
||||
}
|
||||
|
||||
impl RedactionMap {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ids: HashMap::new(),
|
||||
last_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> {
|
||||
let id = id.to_string();
|
||||
|
||||
@@ -28,6 +22,12 @@ impl RedactionMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the numeric ID that was assigned to the provided value,
|
||||
/// if one exists.
|
||||
pub fn get_id_for_value(&self, value: impl ToString) -> Option<usize> {
|
||||
self.ids.get(&value.to_string()).cloned()
|
||||
}
|
||||
|
||||
pub fn intern(&mut self, id: impl ToString) {
|
||||
let last_id = &mut self.last_id;
|
||||
|
||||
|
||||
@@ -3,25 +3,25 @@
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
"Plugin": {
|
||||
"$path": "src"
|
||||
"$path": "plugin/src"
|
||||
},
|
||||
"Packages": {
|
||||
"$path": "Packages",
|
||||
"$path": "plugin/Packages",
|
||||
"Log": {
|
||||
"$path": "log"
|
||||
"$path": "plugin/log"
|
||||
},
|
||||
"Http": {
|
||||
"$path": "http"
|
||||
"$path": "plugin/http"
|
||||
},
|
||||
"Fmt": {
|
||||
"$path": "fmt"
|
||||
"$path": "plugin/fmt"
|
||||
},
|
||||
"RbxDom": {
|
||||
"$path": "rbx_dom_lua"
|
||||
"$path": "plugin/rbx_dom_lua"
|
||||
}
|
||||
},
|
||||
"Version": {
|
||||
"$path": "Version.txt"
|
||||
"$path": "plugin/Version.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Submodule plugin/Packages/t updated: 1f9754254b...1dbfccc182
@@ -1 +1 @@
|
||||
7.4.0
|
||||
7.6.1
|
||||
@@ -70,7 +70,7 @@ local function debugImpl(buffer, value, extendedForm)
|
||||
elseif valueType == "table" then
|
||||
local valueMeta = getmetatable(value)
|
||||
|
||||
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
|
||||
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
|
||||
-- This type implement's the metamethod we made up to line up with
|
||||
-- Rust's 'Debug' trait.
|
||||
|
||||
@@ -242,4 +242,4 @@ return {
|
||||
debugOutputBuffer = debugOutputBuffer,
|
||||
fmt = fmt,
|
||||
debugify = debugify,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,4 +31,4 @@ function Response:json()
|
||||
return HttpService:JSONDecode(self.body)
|
||||
end
|
||||
|
||||
return Response
|
||||
return Response
|
||||
|
||||
@@ -2,4 +2,4 @@ return function()
|
||||
it("should load", function()
|
||||
require(script.Parent)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -57,4 +57,4 @@ function Log.error(template, ...)
|
||||
error(Fmt.fmt(template, ...))
|
||||
end
|
||||
|
||||
return Log
|
||||
return Log
|
||||
|
||||
@@ -2,4 +2,4 @@ return function()
|
||||
it("should load", function()
|
||||
require(script.Parent)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -188,6 +188,38 @@ types = {
|
||||
},
|
||||
|
||||
Content = {
|
||||
fromPod = function(pod): Content
|
||||
if type(pod) == "string" then
|
||||
if pod == "None" then
|
||||
return Content.none
|
||||
else
|
||||
error(`unexpected Content value '{pod}'`)
|
||||
end
|
||||
else
|
||||
local ty, value = next(pod)
|
||||
if ty == "Uri" then
|
||||
return Content.fromUri(value)
|
||||
elseif ty == "Object" then
|
||||
error("Object deserializing is not currently implemented")
|
||||
else
|
||||
error(`Unknown Content type '{ty}' (could not deserialize)`)
|
||||
end
|
||||
end
|
||||
end,
|
||||
toPod = function(roblox: Content)
|
||||
if roblox.SourceType == Enum.ContentSourceType.None then
|
||||
return "None"
|
||||
elseif roblox.SourceType == Enum.ContentSourceType.Uri then
|
||||
return { Uri = roblox.Uri }
|
||||
elseif roblox.SourceType == Enum.ContentSourceType.Object then
|
||||
error("Object serializing is not currently implemented")
|
||||
else
|
||||
error(`Unknown Content type '{roblox.SourceType} (could not serialize)`)
|
||||
end
|
||||
end,
|
||||
},
|
||||
|
||||
ContentId = {
|
||||
fromPod = identity,
|
||||
toPod = identity,
|
||||
},
|
||||
@@ -205,6 +237,19 @@ types = {
|
||||
end,
|
||||
},
|
||||
|
||||
EnumItem = {
|
||||
fromPod = function(pod)
|
||||
return Enum[pod.type]:FromValue(pod.value)
|
||||
end,
|
||||
|
||||
toPod = function(roblox)
|
||||
return {
|
||||
type = tostring(roblox.EnumType),
|
||||
value = roblox.Value,
|
||||
}
|
||||
end,
|
||||
},
|
||||
|
||||
Faces = {
|
||||
fromPod = function(pod)
|
||||
local faces = {}
|
||||
@@ -300,7 +345,12 @@ types = {
|
||||
local keypoints = {}
|
||||
|
||||
for index, keypoint in ipairs(pod.keypoints) do
|
||||
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, keypoint.value, keypoint.envelope)
|
||||
-- TODO: Add a test for NaN or Infinity values and envelopes
|
||||
-- Right now it isn't possible because it'd fail the roundtrip.
|
||||
-- It's more important that it works right now, though.
|
||||
local value = keypoint.value or 0
|
||||
local envelope = keypoint.envelope or 0
|
||||
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, value, envelope)
|
||||
end
|
||||
|
||||
return NumberSequence.new(keypoints)
|
||||
@@ -328,13 +378,26 @@ types = {
|
||||
if pod == "Default" then
|
||||
return nil
|
||||
else
|
||||
return PhysicalProperties.new(
|
||||
pod.density,
|
||||
pod.friction,
|
||||
pod.elasticity,
|
||||
pod.frictionWeight,
|
||||
pod.elasticityWeight
|
||||
)
|
||||
-- Passing `nil` instead of not passing anything gives
|
||||
-- different results, so we have to branch here.
|
||||
if pod.acousticAbsorption then
|
||||
return (PhysicalProperties.new :: any)(
|
||||
pod.density,
|
||||
pod.friction,
|
||||
pod.elasticity,
|
||||
pod.frictionWeight,
|
||||
pod.elasticityWeight,
|
||||
pod.acousticAbsorption
|
||||
)
|
||||
else
|
||||
return PhysicalProperties.new(
|
||||
pod.density,
|
||||
pod.friction,
|
||||
pod.elasticity,
|
||||
pod.frictionWeight,
|
||||
pod.elasticityWeight
|
||||
)
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
@@ -348,6 +411,7 @@ types = {
|
||||
elasticity = roblox.Elasticity,
|
||||
frictionWeight = roblox.FrictionWeight,
|
||||
elasticityWeight = roblox.ElasticityWeight,
|
||||
acousticAbsorption = roblox.AcousticAbsorption,
|
||||
}
|
||||
end
|
||||
end,
|
||||
|
||||
@@ -5,6 +5,7 @@ Error.Kind = {
|
||||
UnknownProperty = "UnknownProperty",
|
||||
PropertyNotReadable = "PropertyNotReadable",
|
||||
PropertyNotWritable = "PropertyNotWritable",
|
||||
CannotParseBinaryString = "CannotParseBinaryString",
|
||||
Roblox = "Roblox",
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
0.0
|
||||
]
|
||||
},
|
||||
"TestEnumItem": {
|
||||
"EnumItem": {
|
||||
"type": "Material",
|
||||
"value": 256
|
||||
}
|
||||
},
|
||||
"TestNumber": {
|
||||
"Float64": 1337.0
|
||||
},
|
||||
@@ -170,9 +176,23 @@
|
||||
},
|
||||
"ty": "ColorSequence"
|
||||
},
|
||||
"Content": {
|
||||
"ContentId": {
|
||||
"value": {
|
||||
"Content": "rbxassetid://12345"
|
||||
"ContentId": "rbxassetid://12345"
|
||||
},
|
||||
"ty": "ContentId"
|
||||
},
|
||||
"Content_None": {
|
||||
"value": {
|
||||
"Content": "None"
|
||||
},
|
||||
"ty": "Content"
|
||||
},
|
||||
"Content_Uri": {
|
||||
"value": {
|
||||
"Content": {
|
||||
"Uri": "rbxasset://abc/123.rojo"
|
||||
}
|
||||
},
|
||||
"ty": "Content"
|
||||
},
|
||||
@@ -182,6 +202,15 @@
|
||||
},
|
||||
"ty": "Enum"
|
||||
},
|
||||
"EnumItem": {
|
||||
"value": {
|
||||
"EnumItem": {
|
||||
"type": "Material",
|
||||
"value": 256
|
||||
}
|
||||
},
|
||||
"ty": "EnumItem"
|
||||
},
|
||||
"Faces": {
|
||||
"value": {
|
||||
"Faces": [
|
||||
@@ -412,7 +441,8 @@
|
||||
"friction": 1.0,
|
||||
"elasticity": 0.0,
|
||||
"frictionWeight": 50.0,
|
||||
"elasticityWeight": 25.0
|
||||
"elasticityWeight": 25.0,
|
||||
"acousticAbsorption": 0.15625
|
||||
}
|
||||
},
|
||||
"ty": "PhysicalProperties"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
local CollectionService = game:GetService("CollectionService")
|
||||
local ScriptEditorService = game:GetService("ScriptEditorService")
|
||||
|
||||
local Error = require(script.Parent.Error)
|
||||
|
||||
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
|
||||
local TERRAIN_MATERIAL_COLORS = {
|
||||
Enum.Material.Grass,
|
||||
@@ -51,6 +53,10 @@ return {
|
||||
return true, instance:GetAttributes()
|
||||
end,
|
||||
write = function(instance, _, value)
|
||||
if typeof(value) ~= "table" then
|
||||
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||
end
|
||||
|
||||
local existing = instance:GetAttributes()
|
||||
local didAllWritesSucceed = true
|
||||
|
||||
@@ -160,9 +166,14 @@ return {
|
||||
return true, colors
|
||||
end,
|
||||
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
|
||||
if typeof(value) ~= "table" then
|
||||
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||
end
|
||||
|
||||
for material, color in value do
|
||||
instance:SetMaterialColor(material, color)
|
||||
end
|
||||
|
||||
return true
|
||||
end,
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local TestEZ = require(ReplicatedStorage.Packages.TestEZ)
|
||||
local TestEZ = require(ReplicatedStorage.Packages:WaitForChild("TestEZ", 10))
|
||||
|
||||
local Rojo = ReplicatedStorage.Rojo
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ local Version = require(script.Parent.Version)
|
||||
local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
|
||||
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
|
||||
local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
|
||||
local validateApiSerialize = Types.ifEnabled(Types.ApiSerializeResponse)
|
||||
local validateApiRefPatch = Types.ifEnabled(Types.ApiRefPatchResponse)
|
||||
|
||||
local function rejectFailedRequests(response)
|
||||
if response.code >= 400 then
|
||||
@@ -45,14 +47,7 @@ end
|
||||
|
||||
local function rejectWrongPlaceId(infoResponseBody)
|
||||
if infoResponseBody.expectedPlaceIds ~= nil then
|
||||
local foundId = false
|
||||
|
||||
for _, id in ipairs(infoResponseBody.expectedPlaceIds) do
|
||||
if id == game.PlaceId then
|
||||
foundId = true
|
||||
break
|
||||
end
|
||||
end
|
||||
local foundId = table.find(infoResponseBody.expectedPlaceIds, game.PlaceId)
|
||||
|
||||
if not foundId then
|
||||
local idList = {}
|
||||
@@ -62,10 +57,30 @@ local function rejectWrongPlaceId(infoResponseBody)
|
||||
|
||||
local message = (
|
||||
"Found a Rojo server, but its project is set to only be used with a specific list of places."
|
||||
.. "\nYour place ID is %s, but needs to be one of these:"
|
||||
.. "\nYour place ID is %u, but needs to be one of these:"
|
||||
.. "\n%s"
|
||||
.. "\n\nTo change this list, edit 'servePlaceIds' in your .project.json file."
|
||||
):format(tostring(game.PlaceId), table.concat(idList, "\n"))
|
||||
):format(game.PlaceId, table.concat(idList, "\n"))
|
||||
|
||||
return Promise.reject(message)
|
||||
end
|
||||
end
|
||||
|
||||
if infoResponseBody.unexpectedPlaceIds ~= nil then
|
||||
local foundId = table.find(infoResponseBody.unexpectedPlaceIds, game.PlaceId)
|
||||
|
||||
if foundId then
|
||||
local idList = {}
|
||||
for _, id in ipairs(infoResponseBody.unexpectedPlaceIds) do
|
||||
table.insert(idList, "- " .. tostring(id))
|
||||
end
|
||||
|
||||
local message = (
|
||||
"Found a Rojo server, but its project is set to not be used with a specific list of places."
|
||||
.. "\nYour place ID is %u, but needs to not be one of these:"
|
||||
.. "\n%s"
|
||||
.. "\n\nTo change this list, edit 'blockedPlaceIds' in your .project.json file."
|
||||
):format(game.PlaceId, table.concat(idList, "\n"))
|
||||
|
||||
return Promise.reject(message)
|
||||
end
|
||||
@@ -239,4 +254,32 @@ function ApiContext:open(id)
|
||||
end)
|
||||
end
|
||||
|
||||
function ApiContext:serialize(ids: { string })
|
||||
local url = ("%s/api/serialize/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||
|
||||
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||
if body.sessionId ~= self.__sessionId then
|
||||
return Promise.reject("Server changed ID")
|
||||
end
|
||||
|
||||
assert(validateApiSerialize(body))
|
||||
|
||||
return body
|
||||
end)
|
||||
end
|
||||
|
||||
function ApiContext:refPatch(ids: { string })
|
||||
local url = ("%s/api/ref-patch/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||
|
||||
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||
if body.sessionId ~= self.__sessionId then
|
||||
return Promise.reject("Server changed ID")
|
||||
end
|
||||
|
||||
assert(validateApiRefPatch(body))
|
||||
|
||||
return body
|
||||
end)
|
||||
end
|
||||
|
||||
return ApiContext
|
||||
|
||||
@@ -32,7 +32,7 @@ end
|
||||
|
||||
function Checkbox:render()
|
||||
return Theme.with(function(theme)
|
||||
theme = theme.Checkbox
|
||||
local checkboxTheme = theme.Checkbox
|
||||
|
||||
local activeTransparency = Roact.joinBindings({
|
||||
self.binding:map(function(value)
|
||||
@@ -57,20 +57,21 @@ function Checkbox:render()
|
||||
end,
|
||||
}, {
|
||||
StateTip = e(Tooltip.Trigger, {
|
||||
text = (if self.props.locked then "[LOCKED] " else "")
|
||||
.. (if self.props.active then "Enabled" else "Disabled"),
|
||||
text = (if self.props.locked
|
||||
then (self.props.lockedTooltip or "(Cannot be changed right now)") .. "\n"
|
||||
else "") .. (if self.props.active then "Enabled" else "Disabled"),
|
||||
}),
|
||||
|
||||
Active = e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = theme.Active.BackgroundColor,
|
||||
color = checkboxTheme.Active.BackgroundColor,
|
||||
transparency = activeTransparency,
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
zIndex = 2,
|
||||
}, {
|
||||
Icon = e("ImageLabel", {
|
||||
Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Active,
|
||||
ImageColor3 = theme.Active.IconColor,
|
||||
ImageColor3 = checkboxTheme.Active.IconColor,
|
||||
ImageTransparency = activeTransparency,
|
||||
|
||||
Size = UDim2.new(0, 16, 0, 16),
|
||||
@@ -83,7 +84,7 @@ function Checkbox:render()
|
||||
|
||||
Inactive = e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBorder,
|
||||
color = theme.Inactive.BorderColor,
|
||||
color = checkboxTheme.Inactive.BorderColor,
|
||||
transparency = self.props.transparency,
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
}, {
|
||||
@@ -91,7 +92,7 @@ function Checkbox:render()
|
||||
Image = if self.props.locked
|
||||
then Assets.Images.Checkbox.Locked
|
||||
else Assets.Images.Checkbox.Inactive,
|
||||
ImageColor3 = theme.Inactive.IconColor,
|
||||
ImageColor3 = checkboxTheme.Inactive.IconColor,
|
||||
ImageTransparency = self.props.transparency,
|
||||
|
||||
Size = UDim2.new(0, 16, 0, 16),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
@@ -7,6 +8,8 @@ Highlighter.matchStudioSettings()
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
|
||||
local CodeLabel = Roact.PureComponent:extend("CodeLabel")
|
||||
|
||||
function CodeLabel:init()
|
||||
@@ -40,22 +43,24 @@ function CodeLabel:updateHighlights()
|
||||
end
|
||||
|
||||
function CodeLabel:render()
|
||||
return e("TextLabel", {
|
||||
Size = self.props.size,
|
||||
Position = self.props.position,
|
||||
Text = self.props.text,
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.RobotoMono,
|
||||
TextSize = 16,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
TextColor3 = Color3.fromRGB(255, 255, 255),
|
||||
[Roact.Ref] = self.labelRef,
|
||||
}, {
|
||||
SyntaxHighlights = e("Folder", {
|
||||
[Roact.Ref] = self.highlightsRef,
|
||||
}),
|
||||
})
|
||||
return Theme.with(function(theme)
|
||||
return e("TextLabel", {
|
||||
Size = self.props.size,
|
||||
Position = self.props.position,
|
||||
Text = self.props.text,
|
||||
BackgroundTransparency = 1,
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Code,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
TextColor3 = Color3.fromRGB(255, 255, 255),
|
||||
[Roact.Ref] = self.labelRef,
|
||||
}, {
|
||||
SyntaxHighlights = e("Folder", {
|
||||
[Roact.Ref] = self.highlightsRef,
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return CodeLabel
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
@@ -10,9 +8,11 @@ local Flipper = require(Packages.Flipper)
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local SlicedImage = require(script.Parent.SlicedImage)
|
||||
local ScrollingFrame = require(script.Parent.ScrollingFrame)
|
||||
local Tooltip = require(script.Parent.Tooltip)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
@@ -44,29 +44,29 @@ end
|
||||
|
||||
function Dropdown:render()
|
||||
return Theme.with(function(theme)
|
||||
theme = theme.Dropdown
|
||||
local dropdownTheme = theme.Dropdown
|
||||
|
||||
local optionButtons = {}
|
||||
local width = -1
|
||||
for i, option in self.props.options do
|
||||
local text = tostring(option or "")
|
||||
local textSize = TextService:GetTextSize(text, 15, Enum.Font.GothamMedium, Vector2.new(math.huge, 20))
|
||||
if textSize.X > width then
|
||||
width = textSize.X
|
||||
local textBounds = getTextBoundsAsync(text, theme.Font.Main, theme.TextSize.Body, math.huge)
|
||||
if textBounds.X > width then
|
||||
width = textBounds.X
|
||||
end
|
||||
|
||||
optionButtons[text] = e("TextButton", {
|
||||
Text = text,
|
||||
LayoutOrder = i,
|
||||
Size = UDim2.new(1, 0, 0, 24),
|
||||
BackgroundColor3 = theme.BackgroundColor,
|
||||
BackgroundColor3 = dropdownTheme.BackgroundColor,
|
||||
TextTransparency = self.props.transparency,
|
||||
BackgroundTransparency = self.props.transparency,
|
||||
BorderSizePixel = 0,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextColor3 = dropdownTheme.TextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextSize = 15,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = theme.TextSize.Body,
|
||||
FontFace = theme.Font.Main,
|
||||
|
||||
[Roact.Event.Activated] = function()
|
||||
if self.props.locked then
|
||||
@@ -103,13 +103,13 @@ function Dropdown:render()
|
||||
}, {
|
||||
Border = e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBorder,
|
||||
color = theme.BorderColor,
|
||||
color = dropdownTheme.BorderColor,
|
||||
transparency = self.props.transparency,
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
}, {
|
||||
DropArrow = e("ImageLabel", {
|
||||
Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow,
|
||||
ImageColor3 = theme.IconColor,
|
||||
ImageColor3 = dropdownTheme.IconColor,
|
||||
ImageTransparency = self.props.transparency,
|
||||
|
||||
Size = UDim2.new(0, 18, 0, 18),
|
||||
@@ -120,15 +120,21 @@ function Dropdown:render()
|
||||
end),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
StateTip = if self.props.locked
|
||||
then e(Tooltip.Trigger, {
|
||||
text = self.props.lockedTooltip or "(Cannot be changed right now)",
|
||||
})
|
||||
else nil,
|
||||
}),
|
||||
Active = e("TextLabel", {
|
||||
Size = UDim2.new(1, -30, 1, 0),
|
||||
Position = UDim2.new(0, 6, 0, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Text = self.props.active,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 15,
|
||||
TextColor3 = theme.TextColor,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = dropdownTheme.TextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = self.props.transparency,
|
||||
}),
|
||||
@@ -136,7 +142,7 @@ function Dropdown:render()
|
||||
Options = if self.state.open
|
||||
then e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = theme.BackgroundColor,
|
||||
color = dropdownTheme.BackgroundColor,
|
||||
position = UDim2.new(1, 0, 1, 3),
|
||||
size = self.openBinding:map(function(a)
|
||||
return UDim2.new(1, 0, a * math.min(3, #self.props.options), 0)
|
||||
@@ -145,7 +151,7 @@ function Dropdown:render()
|
||||
}, {
|
||||
Border = e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBorder,
|
||||
color = theme.BorderColor,
|
||||
color = dropdownTheme.BorderColor,
|
||||
transparency = self.props.transparency,
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
}),
|
||||
|
||||
@@ -9,8 +9,70 @@ local Assets = require(Plugin.Assets)
|
||||
local Config = require(Plugin.Config)
|
||||
local Version = require(Plugin.Version)
|
||||
|
||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||
local SlicedImage = require(script.Parent.SlicedImage)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local function VersionIndicator(props)
|
||||
local updateMessage = Version.getUpdateMessage()
|
||||
|
||||
return Theme.with(function(theme)
|
||||
return e("Frame", {
|
||||
LayoutOrder = props.layoutOrder,
|
||||
Size = UDim2.new(0, 0, 0, 25),
|
||||
BackgroundTransparency = 1,
|
||||
AutomaticSize = Enum.AutomaticSize.X,
|
||||
}, {
|
||||
Border = if updateMessage
|
||||
then e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBorder,
|
||||
color = theme.Button.Bordered.Enabled.BorderColor,
|
||||
transparency = props.transparency,
|
||||
size = UDim2.fromScale(1, 1),
|
||||
zIndex = 0,
|
||||
}, {
|
||||
Indicator = e("ImageLabel", {
|
||||
Size = UDim2.new(0, 10, 0, 10),
|
||||
ScaleType = Enum.ScaleType.Fit,
|
||||
Image = Assets.Images.Circles[16],
|
||||
ImageColor3 = theme.Header.LogoColor,
|
||||
ImageTransparency = props.transparency,
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(1, 0, 0, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
}),
|
||||
})
|
||||
else nil,
|
||||
|
||||
Tip = if updateMessage
|
||||
then e(Tooltip.Trigger, {
|
||||
text = updateMessage,
|
||||
delay = 0.1,
|
||||
})
|
||||
else nil,
|
||||
|
||||
VersionText = e("TextLabel", {
|
||||
Text = Version.display(Config.version),
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Header.VersionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Size = UDim2.new(0, 0, 1, 0),
|
||||
AutomaticSize = Enum.AutomaticSize.X,
|
||||
}, {
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 6),
|
||||
PaddingRight = UDim.new(0, 6),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
local function Header(props)
|
||||
return Theme.with(function(theme)
|
||||
return e("Frame", {
|
||||
@@ -29,18 +91,9 @@ local function Header(props)
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
Version = e("TextLabel", {
|
||||
Text = Version.display(Config.version),
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Header.VersionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
|
||||
Size = UDim2.new(1, 0, 0, 14),
|
||||
|
||||
LayoutOrder = 2,
|
||||
BackgroundTransparency = 1,
|
||||
VersionIndicator = e(VersionIndicator, {
|
||||
transparency = props.transparency,
|
||||
layoutOrder = 2,
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
local StudioService = game:GetService("StudioService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
|
||||
local TextButton = require(Plugin.App.Components.TextButton)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local FullscreenNotification = Roact.Component:extend("FullscreeFullscreenNotificationnNotification")
|
||||
|
||||
function FullscreenNotification:init()
|
||||
self.transparency, self.setTransparency = Roact.createBinding(0)
|
||||
self.lifetime = self.props.timeout
|
||||
end
|
||||
|
||||
function FullscreenNotification:dismiss()
|
||||
if self.props.onClose then
|
||||
self.props.onClose()
|
||||
end
|
||||
end
|
||||
|
||||
function FullscreenNotification:didMount()
|
||||
self.props.soundPlayer:play(Assets.Sounds.Notification)
|
||||
|
||||
self.timeout = task.spawn(function()
|
||||
local clock = os.clock()
|
||||
local seen = false
|
||||
while task.wait(1 / 10) do
|
||||
local now = os.clock()
|
||||
local dt = now - clock
|
||||
clock = now
|
||||
|
||||
if not seen then
|
||||
seen = StudioService.ActiveScript == nil
|
||||
end
|
||||
|
||||
if not seen then
|
||||
-- Don't run down timer before being viewed
|
||||
continue
|
||||
end
|
||||
|
||||
self.lifetime -= dt
|
||||
if self.lifetime <= 0 then
|
||||
self:dismiss()
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
self.timeout = nil
|
||||
end)
|
||||
end
|
||||
|
||||
function FullscreenNotification:willUnmount()
|
||||
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
|
||||
task.cancel(self.timeout)
|
||||
end
|
||||
end
|
||||
|
||||
function FullscreenNotification:render()
|
||||
return Theme.with(function(theme)
|
||||
local actionButtons = {}
|
||||
if self.props.actions then
|
||||
for key, action in self.props.actions do
|
||||
actionButtons[key] = e(TextButton, {
|
||||
text = action.text,
|
||||
style = action.style,
|
||||
onClick = function()
|
||||
self:dismiss()
|
||||
if action.onClick then
|
||||
local success, err = pcall(action.onClick, self)
|
||||
if not success then
|
||||
Log.warn("Error in notification action: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
end,
|
||||
layoutOrder = -action.layoutOrder,
|
||||
transparency = self.transparency,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return e("Frame", {
|
||||
BackgroundColor3 = theme.BackgroundColor,
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
ZIndex = self.props.layoutOrder,
|
||||
}, {
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 17),
|
||||
PaddingRight = UDim.new(0, 15),
|
||||
PaddingTop = UDim.new(0, 10),
|
||||
PaddingBottom = UDim.new(0, 10),
|
||||
}),
|
||||
Layout = e("UIListLayout", {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
Padding = UDim.new(0, 10),
|
||||
}),
|
||||
Logo = e("ImageLabel", {
|
||||
ImageTransparency = self.transparency,
|
||||
Image = Assets.Images.Logo,
|
||||
ImageColor3 = theme.Header.LogoColor,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.fromOffset(60, 27),
|
||||
LayoutOrder = 1,
|
||||
}),
|
||||
Info = e("TextLabel", {
|
||||
Text = self.props.text,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Notification.InfoColor,
|
||||
TextTransparency = self.transparency,
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
TextYAlignment = Enum.TextYAlignment.Center,
|
||||
TextWrapped = true,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
AutomaticSize = Enum.AutomaticSize.Y,
|
||||
Size = UDim2.fromScale(0.4, 0),
|
||||
LayoutOrder = 2,
|
||||
}),
|
||||
Actions = if self.props.actions
|
||||
then e("Frame", {
|
||||
Size = UDim2.new(1, -40, 0, 37),
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 3,
|
||||
}, {
|
||||
Layout = e("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 5),
|
||||
}),
|
||||
Buttons = Roact.createFragment(actionButtons),
|
||||
})
|
||||
else nil,
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return FullscreenNotification
|
||||
@@ -1,4 +1,3 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
local StudioService = game:GetService("StudioService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
@@ -9,16 +8,14 @@ local Roact = require(Packages.Roact)
|
||||
local Flipper = require(Packages.Flipper)
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local bindingUtil = require(script.Parent.bindingUtil)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
local TextButton = require(Plugin.App.Components.TextButton)
|
||||
|
||||
local baseClock = DateTime.now().UnixTimestampMillis
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local Notification = Roact.Component:extend("Notification")
|
||||
@@ -78,7 +75,9 @@ function Notification:didMount()
|
||||
end
|
||||
|
||||
function Notification:willUnmount()
|
||||
task.cancel(self.timeout)
|
||||
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
|
||||
task.cancel(self.timeout)
|
||||
end
|
||||
end
|
||||
|
||||
function Notification:render()
|
||||
@@ -86,51 +85,49 @@ function Notification:render()
|
||||
return 1 - value
|
||||
end)
|
||||
|
||||
local textBounds = TextService:GetTextSize(self.props.text, 15, Enum.Font.GothamMedium, Vector2.new(350, 700))
|
||||
return Theme.with(function(theme)
|
||||
local actionButtons = {}
|
||||
local buttonsX = 0
|
||||
if self.props.actions then
|
||||
local count = 0
|
||||
for key, action in self.props.actions do
|
||||
actionButtons[key] = e(TextButton, {
|
||||
text = action.text,
|
||||
style = action.style,
|
||||
onClick = function()
|
||||
self:dismiss()
|
||||
if action.onClick then
|
||||
local success, err = pcall(action.onClick, self)
|
||||
if not success then
|
||||
Log.warn("Error in notification action: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
end,
|
||||
layoutOrder = -action.layoutOrder,
|
||||
transparency = transparency,
|
||||
})
|
||||
|
||||
local actionButtons = {}
|
||||
local buttonsX = 0
|
||||
if self.props.actions then
|
||||
local count = 0
|
||||
for key, action in self.props.actions do
|
||||
actionButtons[key] = e(TextButton, {
|
||||
text = action.text,
|
||||
style = action.style,
|
||||
onClick = function()
|
||||
local success, err = pcall(action.onClick, self)
|
||||
if not success then
|
||||
Log.warn("Error in notification action: " .. tostring(err))
|
||||
end
|
||||
end,
|
||||
layoutOrder = -action.layoutOrder,
|
||||
transparency = transparency,
|
||||
})
|
||||
buttonsX += getTextBoundsAsync(action.text, theme.Font.Main, theme.TextSize.Large, math.huge).X + (theme.TextSize.Body * 2)
|
||||
|
||||
buttonsX += TextService:GetTextSize(
|
||||
action.text,
|
||||
18,
|
||||
Enum.Font.GothamMedium,
|
||||
Vector2.new(math.huge, math.huge)
|
||||
).X + 30
|
||||
count += 1
|
||||
end
|
||||
|
||||
count += 1
|
||||
buttonsX += (count - 1) * 5
|
||||
end
|
||||
|
||||
buttonsX += (count - 1) * 5
|
||||
end
|
||||
local paddingY, logoSize = 20, 32
|
||||
local actionsY = if self.props.actions then 37 else 0
|
||||
local textXSpace = math.max(250, buttonsX) + 35
|
||||
local textBounds = getTextBoundsAsync(self.props.text, theme.Font.Main, theme.TextSize.Body, textXSpace)
|
||||
local contentX = math.max(textBounds.X, buttonsX)
|
||||
|
||||
local paddingY, logoSize = 20, 32
|
||||
local actionsY = if self.props.actions then 35 else 0
|
||||
local contentX = math.max(textBounds.X, buttonsX)
|
||||
local size = self.binding:map(function(value)
|
||||
return UDim2.fromOffset(
|
||||
(35 + 40 + contentX) * value,
|
||||
5 + actionsY + paddingY + math.max(logoSize, textBounds.Y)
|
||||
)
|
||||
end)
|
||||
|
||||
local size = self.binding:map(function(value)
|
||||
return UDim2.fromOffset(
|
||||
(35 + 40 + contentX) * value,
|
||||
5 + actionsY + paddingY + math.max(logoSize, textBounds.Y)
|
||||
)
|
||||
end)
|
||||
|
||||
return Theme.with(function(theme)
|
||||
return e("TextButton", {
|
||||
BackgroundTransparency = 1,
|
||||
Size = size,
|
||||
@@ -144,31 +141,31 @@ function Notification:render()
|
||||
}, {
|
||||
e(BorderedContainer, {
|
||||
transparency = transparency,
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
size = UDim2.fromScale(1, 1),
|
||||
}, {
|
||||
Contents = e("Frame", {
|
||||
Size = UDim2.new(0, 35 + contentX, 1, -paddingY),
|
||||
Position = UDim2.new(0, 0, 0, paddingY / 2),
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Logo = e("ImageLabel", {
|
||||
ImageTransparency = transparency,
|
||||
Image = Assets.Images.PluginButton,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, logoSize, 0, logoSize),
|
||||
Size = UDim2.fromOffset(logoSize, logoSize),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
AnchorPoint = Vector2.new(0, 0),
|
||||
}),
|
||||
Info = e("TextLabel", {
|
||||
Text = self.props.text,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 15,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Notification.InfoColor,
|
||||
TextTransparency = transparency,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Center,
|
||||
TextWrapped = true,
|
||||
|
||||
Size = UDim2.new(0, textBounds.X, 0, textBounds.Y),
|
||||
Size = UDim2.new(0, textBounds.X, 1, -actionsY),
|
||||
Position = UDim2.fromOffset(35, 0),
|
||||
|
||||
LayoutOrder = 1,
|
||||
@@ -176,8 +173,8 @@ function Notification:render()
|
||||
}),
|
||||
Actions = if self.props.actions
|
||||
then e("Frame", {
|
||||
Size = UDim2.new(1, -40, 0, 35),
|
||||
Position = UDim2.new(1, 0, 1, 0),
|
||||
Size = UDim2.new(1, -40, 0, actionsY),
|
||||
Position = UDim2.fromScale(1, 1),
|
||||
AnchorPoint = Vector2.new(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
@@ -196,32 +193,12 @@ function Notification:render()
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 17),
|
||||
PaddingRight = UDim.new(0, 15),
|
||||
PaddingTop = UDim.new(0, paddingY / 2),
|
||||
PaddingBottom = UDim.new(0, paddingY / 2),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
local Notifications = Roact.Component:extend("Notifications")
|
||||
|
||||
function Notifications:render()
|
||||
local notifs = {}
|
||||
|
||||
for id, notif in self.props.notifications do
|
||||
notifs["NotifID_" .. id] = e(Notification, {
|
||||
soundPlayer = self.props.soundPlayer,
|
||||
text = notif.text,
|
||||
timestamp = notif.timestamp,
|
||||
timeout = notif.timeout,
|
||||
actions = notif.actions,
|
||||
layoutOrder = (notif.timestamp - baseClock),
|
||||
onClose = function()
|
||||
self.props.onClose(id)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return Roact.createFragment(notifs)
|
||||
end
|
||||
|
||||
return Notifications
|
||||
return Notification
|
||||
66
plugin/src/App/Components/Notifications/init.lua
Normal file
66
plugin/src/App/Components/Notifications/init.lua
Normal file
@@ -0,0 +1,66 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
|
||||
local Packages = Rojo.Packages
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local Notification = require(script.Notification)
|
||||
local FullscreenNotification = require(script.FullscreenNotification)
|
||||
|
||||
local Notifications = Roact.Component:extend("Notifications")
|
||||
|
||||
function Notifications:render()
|
||||
local popupNotifs = {}
|
||||
local fullscreenNotifs = {}
|
||||
|
||||
for id, notif in self.props.notifications do
|
||||
local targetTable = if notif.isFullscreen then fullscreenNotifs else popupNotifs
|
||||
local targetComponent = if notif.isFullscreen then FullscreenNotification else Notification
|
||||
targetTable["NotifID_" .. id] = e(targetComponent, {
|
||||
soundPlayer = self.props.soundPlayer,
|
||||
text = notif.text,
|
||||
timeout = notif.timeout,
|
||||
actions = notif.actions,
|
||||
layoutOrder = id,
|
||||
onClose = function()
|
||||
if notif.onClose then
|
||||
notif.onClose()
|
||||
end
|
||||
self.props.onClose(id)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return e("Frame", {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Fullscreen = e("Frame", {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
notifs = Roact.createFragment(fullscreenNotifs),
|
||||
}),
|
||||
Popups = e("Frame", {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Layout = e("UIListLayout", {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Bottom,
|
||||
Padding = UDim.new(0, 5),
|
||||
}),
|
||||
Padding = e("UIPadding", {
|
||||
PaddingTop = UDim.new(0, 5),
|
||||
PaddingBottom = UDim.new(0, 5),
|
||||
PaddingLeft = UDim.new(0, 5),
|
||||
PaddingRight = UDim.new(0, 5),
|
||||
}),
|
||||
notifs = Roact.createFragment(popupNotifs),
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return Notifications
|
||||
@@ -39,8 +39,8 @@ local function ViewDiffButton(props)
|
||||
Label = e("TextLabel", {
|
||||
Text = "View Diff",
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
@@ -170,8 +170,8 @@ function ChangeList:render()
|
||||
ColumnA = e("TextLabel", {
|
||||
Text = tostring(headerRow[1]),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
@@ -182,8 +182,8 @@ function ChangeList:render()
|
||||
ColumnB = e("TextLabel", {
|
||||
Text = tostring(headerRow[2]),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
@@ -194,8 +194,8 @@ function ChangeList:render()
|
||||
ColumnC = e("TextLabel", {
|
||||
Text = tostring(headerRow[3]),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
@@ -230,8 +230,8 @@ function ChangeList:render()
|
||||
ColumnA = e("TextLabel", {
|
||||
Text = (if isWarning then "⚠ " else "") .. tostring(values[1]),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = if isWarning then theme.Diff.Warning else theme.TextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
|
||||
@@ -32,8 +32,8 @@ local function DisplayValue(props)
|
||||
Label = e("TextLabel", {
|
||||
Text = string.format("%d, %d, %d", props.value.R * 255, props.value.G * 255, props.value.B * 255),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = props.textColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
@@ -90,8 +90,8 @@ local function DisplayValue(props)
|
||||
return e("TextLabel", {
|
||||
Text = textRepresentation,
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = props.textColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
@@ -112,8 +112,8 @@ local function DisplayValue(props)
|
||||
return e("TextLabel", {
|
||||
Text = textRepresentation,
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = props.textColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
|
||||
@@ -95,7 +95,7 @@ function DomLabel:render()
|
||||
return Theme.with(function(theme)
|
||||
local color = if props.isWarning
|
||||
then theme.Diff.Warning
|
||||
elseif props.patchType then theme.Diff[props.patchType]
|
||||
elseif props.patchType then theme.Diff.Background[props.patchType]
|
||||
else theme.TextColor
|
||||
|
||||
local indent = (depth - 1) * 12 + 15
|
||||
@@ -225,8 +225,8 @@ function DomLabel:render()
|
||||
Text = (if props.isWarning then "⚠ " else "") .. props.name,
|
||||
RichText = true,
|
||||
BackgroundTransparency = 1,
|
||||
Font = if props.patchType then Enum.Font.GothamBold else Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
FontFace = if props.patchType then theme.Font.Bold else theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = color,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
@@ -251,11 +251,11 @@ function DomLabel:render()
|
||||
then e("TextLabel", {
|
||||
Text = props.changeInfo.edits .. if props.changeInfo.failed then "," else "",
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.SubTextColor,
|
||||
TextTransparency = props.transparency,
|
||||
Size = UDim2.new(0, 0, 0, 16),
|
||||
Size = UDim2.new(0, 0, 0, theme.TextSize.Body),
|
||||
AutomaticSize = Enum.AutomaticSize.X,
|
||||
LayoutOrder = 2,
|
||||
})
|
||||
@@ -264,11 +264,11 @@ function DomLabel:render()
|
||||
then e("TextLabel", {
|
||||
Text = props.changeInfo.failed,
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Diff.Warning,
|
||||
TextTransparency = props.transparency,
|
||||
Size = UDim2.new(0, 0, 0, 16),
|
||||
Size = UDim2.new(0, 0, 0, theme.TextSize.Body),
|
||||
AutomaticSize = Enum.AutomaticSize.X,
|
||||
LayoutOrder = 6,
|
||||
})
|
||||
|
||||
@@ -124,8 +124,8 @@ function PatchVisualizer:render()
|
||||
CleanMerge = e("TextLabel", {
|
||||
Visible = #scrollElements == 0,
|
||||
Text = "No changes to sync, project is up to date.",
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 15,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Medium,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextWrapped = true,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
@@ -11,6 +9,7 @@ local StringDiff = require(script:FindFirstChild("StringDiff"))
|
||||
|
||||
local Timer = require(Plugin.Timer)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local CodeLabel = require(Plugin.App.Components.CodeLabel)
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
@@ -32,7 +31,6 @@ function StringDiffVisualizer:init()
|
||||
end)
|
||||
end)
|
||||
|
||||
self:calculateContentSize()
|
||||
self:updateScriptBackground()
|
||||
|
||||
self:setState({
|
||||
@@ -54,7 +52,6 @@ end
|
||||
|
||||
function StringDiffVisualizer:didUpdate(previousProps)
|
||||
if previousProps.oldString ~= self.props.oldString or previousProps.newString ~= self.props.newString then
|
||||
self:calculateContentSize()
|
||||
local add, remove = self:calculateDiffLines()
|
||||
self:setState({
|
||||
add = add,
|
||||
@@ -63,11 +60,11 @@ function StringDiffVisualizer:didUpdate(previousProps)
|
||||
end
|
||||
end
|
||||
|
||||
function StringDiffVisualizer:calculateContentSize()
|
||||
function StringDiffVisualizer:calculateContentSize(theme)
|
||||
local oldString, newString = self.props.oldString, self.props.newString
|
||||
|
||||
local oldStringBounds = TextService:GetTextSize(oldString, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
|
||||
local newStringBounds = TextService:GetTextSize(newString, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
|
||||
local oldStringBounds = getTextBoundsAsync(oldString, theme.Font.Code, theme.TextSize.Code, math.huge)
|
||||
local newStringBounds = getTextBoundsAsync(newString, theme.Font.Code, theme.TextSize.Code, math.huge)
|
||||
|
||||
self.setContentSize(
|
||||
Vector2.new(math.max(oldStringBounds.X, newStringBounds.X), math.max(oldStringBounds.Y, newStringBounds.Y))
|
||||
@@ -143,6 +140,8 @@ function StringDiffVisualizer:render()
|
||||
local oldString, newString = self.props.oldString, self.props.newString
|
||||
|
||||
return Theme.with(function(theme)
|
||||
self:calculateContentSize(theme)
|
||||
|
||||
return e(BorderedContainer, {
|
||||
size = self.props.size,
|
||||
position = self.props.position,
|
||||
@@ -179,7 +178,7 @@ function StringDiffVisualizer:render()
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
position = UDim2.new(0, 0, 0, 0),
|
||||
text = oldString,
|
||||
lineBackground = theme.Diff.Remove,
|
||||
lineBackground = theme.Diff.Background.Remove,
|
||||
markedLines = self.state.remove,
|
||||
}),
|
||||
}),
|
||||
@@ -194,7 +193,7 @@ function StringDiffVisualizer:render()
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
position = UDim2.new(0, 0, 0, 0),
|
||||
text = newString,
|
||||
lineBackground = theme.Diff.Add,
|
||||
lineBackground = theme.Diff.Background.Add,
|
||||
markedLines = self.state.add,
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -93,7 +93,7 @@ function Array:render()
|
||||
e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 25),
|
||||
BackgroundTransparency = if patchType == "Remain" then 1 else self.props.transparency,
|
||||
BackgroundColor3 = if patchType == "Remain" then theme.Diff.Row else theme.Diff[patchType],
|
||||
BackgroundColor3 = theme.Diff.Background[patchType],
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = i,
|
||||
}, {
|
||||
@@ -152,8 +152,8 @@ function Array:render()
|
||||
BackgroundTransparency = 1,
|
||||
Text = "Old",
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
}),
|
||||
@@ -163,8 +163,8 @@ function Array:render()
|
||||
BackgroundTransparency = 1,
|
||||
Text = "New",
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
}),
|
||||
|
||||
@@ -91,9 +91,7 @@ function Dictionary:render()
|
||||
LayoutOrder = order,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundTransparency = if line.patchType == "Remain" then 1 else self.props.transparency,
|
||||
BackgroundColor3 = if line.patchType == "Remain"
|
||||
then theme.Diff.Row
|
||||
else theme.Diff[line.patchType],
|
||||
BackgroundColor3 = theme.Diff.Background[line.patchType],
|
||||
}, {
|
||||
DiffIcon = if line.patchType ~= "Remain"
|
||||
then e("ImageLabel", {
|
||||
@@ -112,9 +110,9 @@ function Dictionary:render()
|
||||
BackgroundTransparency = 1,
|
||||
Text = key,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Diff.Text[line.patchType],
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
}),
|
||||
OldValue = e("Frame", {
|
||||
@@ -125,7 +123,7 @@ function Dictionary:render()
|
||||
e(DisplayValue, {
|
||||
value = oldValue,
|
||||
transparency = self.props.transparency,
|
||||
textColor = theme.Settings.Setting.DescriptionColor,
|
||||
textColor = theme.Diff.Text[line.patchType],
|
||||
}),
|
||||
}),
|
||||
NewValue = e("Frame", {
|
||||
@@ -136,7 +134,7 @@ function Dictionary:render()
|
||||
e(DisplayValue, {
|
||||
value = newValue,
|
||||
transparency = self.props.transparency,
|
||||
textColor = theme.Settings.Setting.DescriptionColor,
|
||||
textColor = theme.Diff.Text[line.patchType],
|
||||
}),
|
||||
}),
|
||||
})
|
||||
@@ -157,8 +155,8 @@ function Dictionary:render()
|
||||
BackgroundTransparency = 1,
|
||||
Text = "Key",
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
}),
|
||||
@@ -168,8 +166,8 @@ function Dictionary:render()
|
||||
BackgroundTransparency = 1,
|
||||
Text = "Old",
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
}),
|
||||
@@ -179,8 +177,8 @@ function Dictionary:render()
|
||||
BackgroundTransparency = 1,
|
||||
Text = "New",
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
}),
|
||||
|
||||
@@ -4,6 +4,7 @@ local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
|
||||
local SlicedImage = require(Plugin.App.Components.SlicedImage)
|
||||
@@ -11,46 +12,48 @@ local SlicedImage = require(Plugin.App.Components.SlicedImage)
|
||||
local e = Roact.createElement
|
||||
|
||||
return function(props)
|
||||
return e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = props.color,
|
||||
transparency = props.transparency:map(function(transparency)
|
||||
return 0.9 + (0.1 * transparency)
|
||||
end),
|
||||
layoutOrder = props.layoutOrder,
|
||||
position = props.position,
|
||||
anchorPoint = props.anchorPoint,
|
||||
size = UDim2.new(0, 0, 0, 16),
|
||||
automaticSize = Enum.AutomaticSize.X,
|
||||
}, {
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 4),
|
||||
PaddingRight = UDim.new(0, 4),
|
||||
PaddingTop = UDim.new(0, 2),
|
||||
PaddingBottom = UDim.new(0, 2),
|
||||
}),
|
||||
Icon = if props.icon
|
||||
then e("ImageLabel", {
|
||||
Size = UDim2.new(0, 12, 0, 12),
|
||||
Position = UDim2.new(0, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
Image = props.icon,
|
||||
return Theme.with(function(theme)
|
||||
return e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = props.color,
|
||||
transparency = props.transparency:map(function(transparency)
|
||||
return 0.9 + (0.1 * transparency)
|
||||
end),
|
||||
layoutOrder = props.layoutOrder,
|
||||
position = props.position,
|
||||
anchorPoint = props.anchorPoint,
|
||||
size = UDim2.new(0, 0, 0, theme.TextSize.Medium),
|
||||
automaticSize = Enum.AutomaticSize.X,
|
||||
}, {
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 4),
|
||||
PaddingRight = UDim.new(0, 4),
|
||||
PaddingTop = UDim.new(0, 2),
|
||||
PaddingBottom = UDim.new(0, 2),
|
||||
}),
|
||||
Icon = if props.icon
|
||||
then e("ImageLabel", {
|
||||
Size = UDim2.new(0, 12, 0, 12),
|
||||
Position = UDim2.new(0, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
Image = props.icon,
|
||||
BackgroundTransparency = 1,
|
||||
ImageColor3 = props.color,
|
||||
ImageTransparency = props.transparency,
|
||||
})
|
||||
else nil,
|
||||
Text = e("TextLabel", {
|
||||
Text = props.text,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Small,
|
||||
TextColor3 = props.color,
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
TextTransparency = props.transparency,
|
||||
Size = UDim2.new(0, 0, 1, 0),
|
||||
Position = UDim2.new(0, if props.icon then 15 else 0, 0, 0),
|
||||
AutomaticSize = Enum.AutomaticSize.X,
|
||||
BackgroundTransparency = 1,
|
||||
ImageColor3 = props.color,
|
||||
ImageTransparency = props.transparency,
|
||||
})
|
||||
else nil,
|
||||
Text = e("TextLabel", {
|
||||
Text = props.text,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 12,
|
||||
TextColor3 = props.color,
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
TextTransparency = props.transparency,
|
||||
Size = UDim2.new(0, 0, 1, 0),
|
||||
Position = UDim2.new(0, if props.icon then 15 else 0, 0, 0),
|
||||
AutomaticSize = Enum.AutomaticSize.X,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
})
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
@@ -10,6 +8,7 @@ local Flipper = require(Packages.Flipper)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local SlicedImage = require(script.Parent.SlicedImage)
|
||||
local TouchRipple = require(script.Parent.TouchRipple)
|
||||
@@ -41,18 +40,17 @@ end
|
||||
|
||||
function TextButton:render()
|
||||
return Theme.with(function(theme)
|
||||
local textSize =
|
||||
TextService:GetTextSize(self.props.text, 18, Enum.Font.GothamMedium, Vector2.new(math.huge, math.huge))
|
||||
local textBounds = getTextBoundsAsync(self.props.text, theme.Font.Main, theme.TextSize.Large, math.huge)
|
||||
|
||||
local style = self.props.style
|
||||
|
||||
theme = theme.Button[style]
|
||||
local buttonTheme = theme.Button[style]
|
||||
|
||||
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
|
||||
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
|
||||
|
||||
return e("ImageButton", {
|
||||
Size = UDim2.new(0, 15 + textSize.X + 15, 0, 34),
|
||||
Size = UDim2.new(0, (theme.TextSize.Body * 2) + textBounds.X, 0, 34),
|
||||
Position = self.props.position,
|
||||
AnchorPoint = self.props.anchorPoint,
|
||||
|
||||
@@ -74,18 +72,22 @@ function TextButton:render()
|
||||
end,
|
||||
}, {
|
||||
TouchRipple = e(TouchRipple, {
|
||||
color = theme.ActionFillColor,
|
||||
color = buttonTheme.ActionFillColor,
|
||||
transparency = self.props.transparency:map(function(value)
|
||||
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, value })
|
||||
return bindingUtil.blendAlpha({ buttonTheme.ActionFillTransparency, value })
|
||||
end),
|
||||
zIndex = 2,
|
||||
}),
|
||||
|
||||
Text = e("TextLabel", {
|
||||
Text = self.props.text,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 18,
|
||||
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor),
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Large,
|
||||
TextColor3 = bindingUtil.mapLerp(
|
||||
bindingEnabled,
|
||||
buttonTheme.Enabled.TextColor,
|
||||
buttonTheme.Disabled.TextColor
|
||||
),
|
||||
TextTransparency = self.props.transparency,
|
||||
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
@@ -95,7 +97,11 @@ function TextButton:render()
|
||||
|
||||
Border = style == "Bordered" and e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBorder,
|
||||
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor),
|
||||
color = bindingUtil.mapLerp(
|
||||
bindingEnabled,
|
||||
buttonTheme.Enabled.BorderColor,
|
||||
buttonTheme.Disabled.BorderColor
|
||||
),
|
||||
transparency = self.props.transparency,
|
||||
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
@@ -105,14 +111,18 @@ function TextButton:render()
|
||||
|
||||
HoverOverlay = e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = theme.ActionFillColor,
|
||||
color = buttonTheme.ActionFillColor,
|
||||
transparency = Roact.joinBindings({
|
||||
hover = bindingHover:map(function(value)
|
||||
return 1 - value
|
||||
end),
|
||||
transparency = self.props.transparency,
|
||||
}):map(function(values)
|
||||
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
|
||||
return bindingUtil.blendAlpha({
|
||||
buttonTheme.ActionFillTransparency,
|
||||
values.hover,
|
||||
values.transparency,
|
||||
})
|
||||
end),
|
||||
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
@@ -124,8 +134,8 @@ function TextButton:render()
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = bindingUtil.mapLerp(
|
||||
bindingEnabled,
|
||||
theme.Enabled.BackgroundColor,
|
||||
theme.Disabled.BackgroundColor
|
||||
buttonTheme.Enabled.BackgroundColor,
|
||||
buttonTheme.Disabled.BackgroundColor
|
||||
),
|
||||
transparency = self.props.transparency,
|
||||
|
||||
|
||||
@@ -38,14 +38,18 @@ end
|
||||
|
||||
function TextInput:render()
|
||||
return Theme.with(function(theme)
|
||||
theme = theme.TextInput
|
||||
local textInputTheme = theme.TextInput
|
||||
|
||||
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
|
||||
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
|
||||
|
||||
return e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBorder,
|
||||
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor),
|
||||
color = bindingUtil.mapLerp(
|
||||
bindingEnabled,
|
||||
textInputTheme.Enabled.BorderColor,
|
||||
textInputTheme.Disabled.BorderColor
|
||||
),
|
||||
transparency = self.props.transparency,
|
||||
|
||||
size = self.props.size or UDim2.new(1, 0, 1, 0),
|
||||
@@ -55,14 +59,18 @@ function TextInput:render()
|
||||
}, {
|
||||
HoverOverlay = e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = theme.ActionFillColor,
|
||||
color = textInputTheme.ActionFillColor,
|
||||
transparency = Roact.joinBindings({
|
||||
hover = bindingHover:map(function(value)
|
||||
return 1 - value
|
||||
end),
|
||||
transparency = self.props.transparency,
|
||||
}):map(function(values)
|
||||
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
|
||||
return bindingUtil.blendAlpha({
|
||||
textInputTheme.ActionFillTransparency,
|
||||
values.hover,
|
||||
values.transparency,
|
||||
})
|
||||
end),
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
zIndex = -1,
|
||||
@@ -72,14 +80,18 @@ function TextInput:render()
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
Text = self.props.text,
|
||||
PlaceholderText = self.props.placeholder,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Disabled.TextColor, theme.Enabled.TextColor),
|
||||
FontFace = theme.Font.Main,
|
||||
TextColor3 = bindingUtil.mapLerp(
|
||||
bindingEnabled,
|
||||
textInputTheme.Disabled.TextColor,
|
||||
textInputTheme.Enabled.TextColor
|
||||
),
|
||||
PlaceholderColor3 = bindingUtil.mapLerp(
|
||||
bindingEnabled,
|
||||
theme.Disabled.PlaceholderColor,
|
||||
theme.Enabled.PlaceholderColor
|
||||
textInputTheme.Disabled.PlaceholderColor,
|
||||
textInputTheme.Enabled.PlaceholderColor
|
||||
),
|
||||
TextSize = 18,
|
||||
TextSize = theme.TextSize.Large,
|
||||
TextEditable = self.props.enabled,
|
||||
ClearTextOnFocus = self.props.clearTextOnFocus,
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
@@ -8,6 +7,8 @@ local Packages = Rojo.Packages
|
||||
local Roact = require(Packages.Roact)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
|
||||
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
|
||||
local e = Roact.createElement
|
||||
@@ -21,50 +22,48 @@ local Y_OVERLAP = 10 -- Let the triangle tail piece overlap the target a bit to
|
||||
local TooltipContext = Roact.createContext({})
|
||||
|
||||
local function Popup(props)
|
||||
local textSize = TextService:GetTextSize(
|
||||
props.Text,
|
||||
16,
|
||||
Enum.Font.GothamMedium,
|
||||
Vector2.new(math.min(props.parentSize.X, 160), math.huge)
|
||||
) + TEXT_PADDING + (Vector2.one * 2)
|
||||
|
||||
local trigger = props.Trigger:getValue()
|
||||
|
||||
local spaceBelow = props.parentSize.Y
|
||||
- (trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y - Y_OVERLAP + TAIL_SIZE)
|
||||
local spaceAbove = trigger.AbsolutePosition.Y + Y_OVERLAP - TAIL_SIZE
|
||||
|
||||
-- If there's not enough space below, and there's more space above, then show the tooltip above the trigger
|
||||
local displayAbove = spaceBelow < textSize.Y and spaceAbove > spaceBelow
|
||||
|
||||
local X = math.clamp(props.Position.X - X_OFFSET, 0, props.parentSize.X - textSize.X)
|
||||
local Y = 0
|
||||
|
||||
if displayAbove then
|
||||
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - textSize.Y + Y_OVERLAP, 0)
|
||||
else
|
||||
Y = math.min(
|
||||
trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP,
|
||||
props.parentSize.Y - textSize.Y
|
||||
)
|
||||
end
|
||||
|
||||
return Theme.with(function(theme)
|
||||
local textXSpace = math.min(props.parentSize.X, 250) - TEXT_PADDING.X
|
||||
local textBounds = getTextBoundsAsync(props.Text, theme.Font.Main, theme.TextSize.Medium, textXSpace)
|
||||
local contentSize = textBounds + TEXT_PADDING + (Vector2.one * 2)
|
||||
|
||||
local trigger = props.Trigger:getValue()
|
||||
|
||||
local spaceBelow = props.parentSize.Y
|
||||
- (trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y - Y_OVERLAP + TAIL_SIZE)
|
||||
local spaceAbove = trigger.AbsolutePosition.Y + Y_OVERLAP - TAIL_SIZE
|
||||
|
||||
-- If there's not enough space below, and there's more space above, then show the tooltip above the trigger
|
||||
local displayAbove = spaceBelow < contentSize.Y and spaceAbove > spaceBelow
|
||||
|
||||
local X = math.clamp(props.Position.X - X_OFFSET, 0, math.max(props.parentSize.X - contentSize.X, 1))
|
||||
local Y = 0
|
||||
|
||||
if displayAbove then
|
||||
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - contentSize.Y + Y_OVERLAP, 0)
|
||||
else
|
||||
Y = math.min(
|
||||
trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP,
|
||||
props.parentSize.Y - contentSize.Y
|
||||
)
|
||||
end
|
||||
|
||||
return e(BorderedContainer, {
|
||||
position = UDim2.fromOffset(X, Y),
|
||||
size = UDim2.fromOffset(textSize.X, textSize.Y),
|
||||
size = UDim2.fromOffset(contentSize.X, contentSize.Y),
|
||||
transparency = props.transparency,
|
||||
}, {
|
||||
Label = e("TextLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
Size = UDim2.new(1, -TEXT_PADDING.X, 1, -TEXT_PADDING.Y),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Size = UDim2.fromOffset(textBounds.X, textBounds.Y),
|
||||
Text = props.Text,
|
||||
TextSize = 16,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = theme.TextSize.Medium,
|
||||
FontFace = theme.Font.Main,
|
||||
TextWrapped = true,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Center,
|
||||
TextColor3 = theme.Button.Bordered.Enabled.TextColor,
|
||||
TextTransparency = props.transparency,
|
||||
}),
|
||||
@@ -72,8 +71,8 @@ local function Popup(props)
|
||||
Tail = e("ImageLabel", {
|
||||
ZIndex = 100,
|
||||
Position = if displayAbove
|
||||
then UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 1, -1)
|
||||
else UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 0, -TAIL_SIZE + 1),
|
||||
then UDim2.new(0, math.clamp(props.Position.X - X, 6, contentSize.X - 6), 1, -1)
|
||||
else UDim2.new(0, math.clamp(props.Position.X - X, 6, contentSize.X - 6), 0, -TAIL_SIZE + 1),
|
||||
Size = UDim2.fromOffset(TAIL_SIZE, TAIL_SIZE),
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
Rotation = if displayAbove then 180 else 0,
|
||||
@@ -217,7 +216,7 @@ function Trigger:managePopup()
|
||||
return
|
||||
end
|
||||
|
||||
self.showDelayThread = task.delay(DELAY, function()
|
||||
self.showDelayThread = task.delay(self.props.delay or DELAY, function()
|
||||
self.props.context.addTip(self.id, {
|
||||
Text = self.props.text,
|
||||
Position = self:getMousePos(),
|
||||
|
||||
@@ -4,8 +4,6 @@ local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Timer = require(Plugin.Timer)
|
||||
local PatchTree = require(Plugin.PatchTree)
|
||||
local Settings = require(Plugin.Settings)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local TextButton = require(Plugin.App.Components.TextButton)
|
||||
@@ -24,7 +22,6 @@ function ConfirmingPage:init()
|
||||
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
|
||||
self:setState({
|
||||
patchTree = nil,
|
||||
showingStringDiff = false,
|
||||
oldString = "",
|
||||
newString = "",
|
||||
@@ -32,28 +29,6 @@ function ConfirmingPage:init()
|
||||
oldTable = {},
|
||||
newTable = {},
|
||||
})
|
||||
|
||||
if self.props.confirmData and self.props.confirmData.patch and self.props.confirmData.instanceMap then
|
||||
self:buildPatchTree()
|
||||
end
|
||||
end
|
||||
|
||||
function ConfirmingPage:didUpdate(prevProps)
|
||||
if prevProps.confirmData ~= self.props.confirmData then
|
||||
self:buildPatchTree()
|
||||
end
|
||||
end
|
||||
|
||||
function ConfirmingPage:buildPatchTree()
|
||||
Timer.start("ConfirmingPage:buildPatchTree")
|
||||
self:setState({
|
||||
patchTree = PatchTree.build(
|
||||
self.props.confirmData.patch,
|
||||
self.props.confirmData.instanceMap,
|
||||
{ "Property", "Current", "Incoming" }
|
||||
),
|
||||
})
|
||||
Timer.stop()
|
||||
end
|
||||
|
||||
function ConfirmingPage:render()
|
||||
@@ -64,13 +39,13 @@ function ConfirmingPage:render()
|
||||
"Sync changes for project '%s':",
|
||||
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
|
||||
),
|
||||
Font = Enum.Font.Gotham,
|
||||
FontFace = theme.Font.Thin,
|
||||
LineHeight = 1.2,
|
||||
TextSize = 14,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = self.props.transparency,
|
||||
Size = UDim2.new(1, 0, 0, 20),
|
||||
Size = UDim2.new(1, 0, 0, theme.TextSize.Large + 2),
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
@@ -79,7 +54,7 @@ function ConfirmingPage:render()
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 3,
|
||||
|
||||
patchTree = self.state.patchTree,
|
||||
patchTree = self.props.patchTree,
|
||||
|
||||
showStringDiff = function(oldString: string, newString: string)
|
||||
self:setState({
|
||||
|
||||
@@ -61,12 +61,12 @@ function ChangesViewer:render()
|
||||
|
||||
Title = e("TextLabel", {
|
||||
Text = "Sync",
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 17,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Large,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextTransparency = self.props.transparency,
|
||||
Size = UDim2.new(1, -40, 0, 20),
|
||||
Size = UDim2.new(1, -40, 0, theme.TextSize.Large + 2),
|
||||
Position = UDim2.new(0, 40, 0, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
@@ -74,13 +74,13 @@ function ChangesViewer:render()
|
||||
Subtitle = e("TextLabel", {
|
||||
Text = DateTime.fromUnixTimestamp(self.props.patchData.timestamp):FormatLocalTime("LTS", "en-us"),
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 15,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Medium,
|
||||
TextColor3 = theme.SubTextColor,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
TextTransparency = self.props.transparency,
|
||||
Size = UDim2.new(1, -40, 0, 16),
|
||||
Position = UDim2.new(0, 40, 0, 20),
|
||||
Size = UDim2.new(1, -40, 0, theme.TextSize.Medium),
|
||||
Position = UDim2.new(0, 40, 0, theme.TextSize.Large + 2),
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
@@ -131,8 +131,8 @@ function ChangesViewer:render()
|
||||
}),
|
||||
AppliedText = e("TextLabel", {
|
||||
Text = applied,
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 15,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextTransparency = self.props.transparency,
|
||||
Size = UDim2.new(0, 0, 1, 0),
|
||||
@@ -156,8 +156,8 @@ function ChangesViewer:render()
|
||||
}),
|
||||
UnappliedText = e("TextLabel", {
|
||||
Text = unapplied,
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 15,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Diff.Warning,
|
||||
TextTransparency = self.props.transparency,
|
||||
Size = UDim2.new(0, 0, 1, 0),
|
||||
@@ -217,13 +217,13 @@ local function ConnectionDetails(props)
|
||||
}, {
|
||||
ProjectName = e("TextLabel", {
|
||||
Text = props.projectName,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 20,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Large,
|
||||
TextColor3 = theme.ConnectionDetails.ProjectNameColor,
|
||||
TextTransparency = props.transparency,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
|
||||
Size = UDim2.new(1, 0, 0, 20),
|
||||
Size = UDim2.new(1, 0, 0, theme.TextSize.Large),
|
||||
|
||||
LayoutOrder = 1,
|
||||
BackgroundTransparency = 1,
|
||||
@@ -231,13 +231,13 @@ local function ConnectionDetails(props)
|
||||
|
||||
Address = e("TextLabel", {
|
||||
Text = props.address,
|
||||
Font = Enum.Font.Code,
|
||||
TextSize = 15,
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Medium,
|
||||
TextColor3 = theme.ConnectionDetails.AddressColor,
|
||||
TextTransparency = props.transparency,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
|
||||
Size = UDim2.new(1, 0, 0, 15),
|
||||
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium),
|
||||
|
||||
LayoutOrder = 2,
|
||||
BackgroundTransparency = 1,
|
||||
@@ -410,8 +410,8 @@ function ConnectedPage:render()
|
||||
Text = e("TextLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
Text = self.changeInfoText,
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 15,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
|
||||
TextTransparency = self.props.transparency,
|
||||
TextXAlignment = Enum.TextXAlignment.Right,
|
||||
|
||||
@@ -4,6 +4,8 @@ local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
|
||||
local Spinner = require(Plugin.App.Components.Spinner)
|
||||
|
||||
local e = Roact.createElement
|
||||
@@ -11,11 +13,35 @@ local e = Roact.createElement
|
||||
local ConnectingPage = Roact.Component:extend("ConnectingPage")
|
||||
|
||||
function ConnectingPage:render()
|
||||
return e(Spinner, {
|
||||
position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
anchorPoint = Vector2.new(0.5, 0.5),
|
||||
transparency = self.props.transparency,
|
||||
})
|
||||
return Theme.with(function(theme)
|
||||
return e("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Spinner = e(Spinner, {
|
||||
position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
anchorPoint = Vector2.new(0.5, 0.5),
|
||||
transparency = self.props.transparency,
|
||||
}),
|
||||
Text = if type(self.props.text) == "string" and #self.props.text > 0
|
||||
then e("TextLabel", {
|
||||
Text = self.props.text,
|
||||
Position = UDim2.new(0.5, 0, 0.5, 30),
|
||||
Size = UDim2.new(1, -40, 0.5, -40),
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
RichText = true,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Medium,
|
||||
TextColor3 = theme.SubTextColor,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
TextTransparency = self.props.transparency,
|
||||
BackgroundTransparency = 1,
|
||||
})
|
||||
else nil,
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return ConnectingPage
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
@@ -7,8 +5,10 @@ local Packages = Rojo.Packages
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local TextButton = require(Plugin.App.Components.TextButton)
|
||||
local Header = require(Plugin.App.Components.Header)
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||
@@ -24,43 +24,44 @@ function Error:init()
|
||||
end
|
||||
|
||||
function Error:render()
|
||||
return e(BorderedContainer, {
|
||||
size = Roact.joinBindings({
|
||||
containerSize = self.props.containerSize,
|
||||
contentSize = self.contentSize,
|
||||
}):map(function(values)
|
||||
local maximumSize = values.containerSize
|
||||
maximumSize -= Vector2.new(14, 14) * 2 -- Page padding
|
||||
maximumSize -= Vector2.new(0, 34 + 10) -- Buttons and spacing
|
||||
return Theme.with(function(theme)
|
||||
return e(BorderedContainer, {
|
||||
size = Roact.joinBindings({
|
||||
containerSize = self.props.containerSize,
|
||||
contentSize = self.contentSize,
|
||||
}):map(function(values)
|
||||
local maximumSize = values.containerSize
|
||||
maximumSize -= Vector2.new(14, 14) * 2 -- Page padding
|
||||
maximumSize -= Vector2.new(0, 34 + 10) -- Buttons and spacing
|
||||
|
||||
local outerSize = values.contentSize + ERROR_PADDING * 2
|
||||
local outerSize = values.contentSize + ERROR_PADDING * 2
|
||||
|
||||
return UDim2.new(1, 0, 0, math.min(outerSize.Y, maximumSize.Y))
|
||||
end),
|
||||
transparency = self.props.transparency,
|
||||
}, {
|
||||
ScrollingFrame = e(ScrollingFrame, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
contentSize = self.contentSize:map(function(value)
|
||||
return value + ERROR_PADDING * 2
|
||||
return UDim2.new(1, 0, 0, math.min(outerSize.Y, maximumSize.Y))
|
||||
end),
|
||||
transparency = self.props.transparency,
|
||||
|
||||
[Roact.Change.AbsoluteSize] = function(object)
|
||||
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
|
||||
|
||||
local textBounds = TextService:GetTextSize(
|
||||
self.props.errorMessage,
|
||||
16,
|
||||
Enum.Font.Code,
|
||||
Vector2.new(containerSize.X, math.huge)
|
||||
)
|
||||
|
||||
self.setContentSize(Vector2.new(containerSize.X, textBounds.Y))
|
||||
end,
|
||||
layoutOrder = self.props.layoutOrder,
|
||||
}, {
|
||||
ErrorMessage = Theme.with(function(theme)
|
||||
return e("TextBox", {
|
||||
ScrollingFrame = e(ScrollingFrame, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
contentSize = self.contentSize:map(function(value)
|
||||
return value + ERROR_PADDING * 2
|
||||
end),
|
||||
transparency = self.props.transparency,
|
||||
|
||||
[Roact.Change.AbsoluteSize] = function(object)
|
||||
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
|
||||
|
||||
local textBounds = getTextBoundsAsync(
|
||||
self.props.errorMessage,
|
||||
theme.Font.Code,
|
||||
theme.TextSize.Code,
|
||||
containerSize.X
|
||||
)
|
||||
|
||||
self.setContentSize(Vector2.new(containerSize.X, textBounds.Y))
|
||||
end,
|
||||
}, {
|
||||
ErrorMessage = e("TextBox", {
|
||||
[Roact.Event.InputBegan] = function(rbx, input)
|
||||
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
|
||||
return
|
||||
@@ -71,8 +72,8 @@ function Error:render()
|
||||
|
||||
Text = self.props.errorMessage,
|
||||
TextEditable = false,
|
||||
Font = Enum.Font.Code,
|
||||
TextSize = 16,
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Code,
|
||||
TextColor3 = theme.ErrorColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
@@ -81,17 +82,17 @@ function Error:render()
|
||||
ClearTextOnFocus = false,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
})
|
||||
end),
|
||||
}),
|
||||
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, ERROR_PADDING.X),
|
||||
PaddingRight = UDim.new(0, ERROR_PADDING.X),
|
||||
PaddingTop = UDim.new(0, ERROR_PADDING.Y),
|
||||
PaddingBottom = UDim.new(0, ERROR_PADDING.Y),
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, ERROR_PADDING.X),
|
||||
PaddingRight = UDim.new(0, ERROR_PADDING.X),
|
||||
PaddingTop = UDim.new(0, ERROR_PADDING.Y),
|
||||
PaddingBottom = UDim.new(0, ERROR_PADDING.Y),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
local ErrorPage = Roact.Component:extend("ErrorPage")
|
||||
@@ -109,16 +110,21 @@ function ErrorPage:render()
|
||||
self.setContainerSize(object.AbsoluteSize)
|
||||
end,
|
||||
}, {
|
||||
Error = e(Error, {
|
||||
errorMessage = self.state.errorMessage,
|
||||
containerSize = self.containerSize,
|
||||
Header = e(Header, {
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
}),
|
||||
|
||||
Error = e(Error, {
|
||||
errorMessage = self.state.errorMessage,
|
||||
containerSize = self.containerSize,
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 2,
|
||||
}),
|
||||
|
||||
Buttons = e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 35),
|
||||
LayoutOrder = 2,
|
||||
LayoutOrder = 3,
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Close = e(TextButton, {
|
||||
|
||||
@@ -27,8 +27,8 @@ local function AddressEntry(props)
|
||||
}, {
|
||||
Host = e("TextBox", {
|
||||
Text = props.host or "",
|
||||
Font = Enum.Font.Code,
|
||||
TextSize = 18,
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Large,
|
||||
TextColor3 = theme.AddressEntry.TextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
@@ -51,8 +51,8 @@ local function AddressEntry(props)
|
||||
|
||||
Port = e("TextBox", {
|
||||
Text = props.port or "",
|
||||
Font = Enum.Font.Code,
|
||||
TextSize = 18,
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Large,
|
||||
TextColor3 = theme.AddressEntry.TextColor,
|
||||
TextTransparency = props.transparency,
|
||||
PlaceholderText = Config.defaultPort,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
@@ -9,6 +7,7 @@ local Roact = require(Packages.Roact)
|
||||
local Settings = require(Plugin.Settings)
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local Checkbox = require(Plugin.App.Components.Checkbox)
|
||||
local Dropdown = require(Plugin.App.Components.Dropdown)
|
||||
@@ -31,10 +30,16 @@ local TAG_TYPES = {
|
||||
},
|
||||
}
|
||||
|
||||
local function getTextBounds(text, textSize, font, lineHeight, bounds)
|
||||
local textBounds = TextService:GetTextSize(text, textSize, font, bounds)
|
||||
local function getTextBoundsWithLineHeight(
|
||||
text: string,
|
||||
font: Font,
|
||||
textSize: number,
|
||||
width: number,
|
||||
lineHeight: number
|
||||
)
|
||||
local textBounds = getTextBoundsAsync(text, font, textSize, width)
|
||||
|
||||
local lineCount = textBounds.Y / textSize
|
||||
local lineCount = math.ceil(textBounds.Y / textSize)
|
||||
local lineHeightAbsolute = textSize * lineHeight
|
||||
|
||||
return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize))
|
||||
@@ -109,6 +114,7 @@ function Setting:render()
|
||||
then self.props.input
|
||||
elseif self.props.options ~= nil then e(Dropdown, {
|
||||
locked = self.props.locked,
|
||||
lockedTooltip = self.props.lockedTooltip,
|
||||
options = self.props.options,
|
||||
active = self.state.setting,
|
||||
transparency = self.props.transparency,
|
||||
@@ -118,6 +124,7 @@ function Setting:render()
|
||||
})
|
||||
else e(Checkbox, {
|
||||
locked = self.props.locked,
|
||||
lockedTooltip = self.props.lockedTooltip,
|
||||
active = self.state.setting,
|
||||
transparency = self.props.transparency,
|
||||
onClick = function()
|
||||
@@ -145,7 +152,7 @@ function Setting:render()
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Heading = e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 16),
|
||||
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Layout = e("UIListLayout", {
|
||||
@@ -165,8 +172,8 @@ function Setting:render()
|
||||
else nil,
|
||||
Name = e("TextLabel", {
|
||||
Text = self.props.name,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 16,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Medium,
|
||||
TextColor3 = if self.props.tag and TAG_TYPES[self.props.tag]
|
||||
then getThemeColorFromPath(theme, TAG_TYPES[self.props.tag].color)
|
||||
else settingsTheme.Setting.NameColor,
|
||||
@@ -174,7 +181,7 @@ function Setting:render()
|
||||
TextTransparency = self.props.transparency,
|
||||
RichText = true,
|
||||
|
||||
Size = UDim2.new(1, 0, 0, 16),
|
||||
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium),
|
||||
|
||||
LayoutOrder = 2,
|
||||
BackgroundTransparency = 1,
|
||||
@@ -183,9 +190,9 @@ function Setting:render()
|
||||
|
||||
Description = e("TextLabel", {
|
||||
Text = self.props.description,
|
||||
Font = Enum.Font.Gotham,
|
||||
FontFace = theme.Font.Main,
|
||||
LineHeight = 1.2,
|
||||
TextSize = 14,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = settingsTheme.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = self.props.transparency,
|
||||
@@ -197,12 +204,12 @@ function Setting:render()
|
||||
inputSize = self.inputSize,
|
||||
}):map(function(values)
|
||||
local offset = values.inputSize.X + 5
|
||||
local textBounds = getTextBounds(
|
||||
local textBounds = getTextBoundsWithLineHeight(
|
||||
self.props.description,
|
||||
14,
|
||||
Enum.Font.Gotham,
|
||||
1.2,
|
||||
Vector2.new(values.containerSize.X - offset, math.huge)
|
||||
theme.Font.Main,
|
||||
theme.TextSize.Body,
|
||||
values.containerSize.X - offset,
|
||||
1.2
|
||||
)
|
||||
return UDim2.new(1, -offset, 0, textBounds.Y)
|
||||
end),
|
||||
|
||||
@@ -27,10 +27,11 @@ end
|
||||
|
||||
local invertedLevels = invertTbl(Log.Level)
|
||||
local confirmationBehaviors = { "Initial", "Always", "Large Changes", "Unlisted PlaceId", "Never" }
|
||||
local syncReminderModes = { "None", "Notify", "Fullscreen" }
|
||||
|
||||
local function Navbar(props)
|
||||
return Theme.with(function(theme)
|
||||
theme = theme.Settings.Navbar
|
||||
local navbarTheme = theme.Settings.Navbar
|
||||
|
||||
return e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 46),
|
||||
@@ -40,7 +41,7 @@ local function Navbar(props)
|
||||
Back = e(IconButton, {
|
||||
icon = Assets.Images.Icons.Back,
|
||||
iconSize = 24,
|
||||
color = theme.BackButtonColor,
|
||||
color = navbarTheme.BackButtonColor,
|
||||
transparency = props.transparency,
|
||||
|
||||
position = UDim2.new(0, 0, 0.5, 0),
|
||||
@@ -55,9 +56,9 @@ local function Navbar(props)
|
||||
|
||||
Text = e("TextLabel", {
|
||||
Text = "Settings",
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 18,
|
||||
TextColor3 = theme.TextColor,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Large,
|
||||
TextColor3 = navbarTheme.TextColor,
|
||||
TextTransparency = props.transparency,
|
||||
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
@@ -81,185 +82,211 @@ function SettingsPage:render()
|
||||
return layoutOrder
|
||||
end
|
||||
|
||||
return Theme.with(function(theme)
|
||||
theme = theme.Settings
|
||||
|
||||
return Roact.createFragment({
|
||||
Navbar = e(Navbar, {
|
||||
onBack = self.props.onBack,
|
||||
return Roact.createFragment({
|
||||
Navbar = e(Navbar, {
|
||||
onBack = self.props.onBack,
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
Content = e(ScrollingFrame, {
|
||||
size = UDim2.new(1, 0, 1, -47),
|
||||
position = UDim2.new(0, 0, 0, 47),
|
||||
contentSize = self.contentSize,
|
||||
transparency = self.props.transparency,
|
||||
}, {
|
||||
AutoReconnect = e(Setting, {
|
||||
id = "autoReconnect",
|
||||
name = "Auto Reconnect",
|
||||
description = "Reconnect to server on place open if the served project matches the last sync to the place",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
Content = e(ScrollingFrame, {
|
||||
size = UDim2.new(1, 0, 1, -47),
|
||||
position = UDim2.new(0, 0, 0, 47),
|
||||
contentSize = self.contentSize,
|
||||
|
||||
ShowNotifications = e(Setting, {
|
||||
id = "showNotifications",
|
||||
name = "Show Notifications",
|
||||
description = "Popup notifications in viewport",
|
||||
transparency = self.props.transparency,
|
||||
}, {
|
||||
ShowNotifications = e(Setting, {
|
||||
id = "showNotifications",
|
||||
name = "Show Notifications",
|
||||
description = "Popup notifications in viewport",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
SyncReminder = e(Setting, {
|
||||
id = "syncReminder",
|
||||
name = "Sync Reminder",
|
||||
description = "Notify to sync when opening a place that has previously been synced",
|
||||
transparency = self.props.transparency,
|
||||
visible = Settings:getBinding("showNotifications"),
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
SyncReminderMode = e(Setting, {
|
||||
id = "syncReminderMode",
|
||||
name = "Sync Reminder",
|
||||
description = "What type of reminders you receive for syncing your project",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
visible = Settings:getBinding("showNotifications"),
|
||||
|
||||
ConfirmationBehavior = e(Setting, {
|
||||
id = "confirmationBehavior",
|
||||
name = "Confirmation Behavior",
|
||||
description = "When to prompt for confirmation before syncing",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
options = syncReminderModes,
|
||||
}),
|
||||
|
||||
options = confirmationBehaviors,
|
||||
}),
|
||||
SyncReminderPolling = e(Setting, {
|
||||
id = "syncReminderPolling",
|
||||
name = "Sync Reminder Polling",
|
||||
description = "Look for available sync servers periodically",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
visible = Settings:getBindings("syncReminderMode", "showNotifications"):map(function(values)
|
||||
return values.syncReminderMode ~= "None" and values.showNotifications
|
||||
end),
|
||||
}),
|
||||
|
||||
LargeChangesConfirmationThreshold = e(Setting, {
|
||||
id = "largeChangesConfirmationThreshold",
|
||||
name = "Confirmation Threshold",
|
||||
description = "How many modified instances to be considered a large change",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
visible = Settings:getBinding("confirmationBehavior"):map(function(value)
|
||||
return value == "Large Changes"
|
||||
ConfirmationBehavior = e(Setting, {
|
||||
id = "confirmationBehavior",
|
||||
name = "Confirmation Behavior",
|
||||
description = "When to prompt for confirmation before syncing",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
|
||||
options = confirmationBehaviors,
|
||||
}),
|
||||
|
||||
LargeChangesConfirmationThreshold = e(Setting, {
|
||||
id = "largeChangesConfirmationThreshold",
|
||||
name = "Confirmation Threshold",
|
||||
description = "How many modified instances to be considered a large change",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
visible = Settings:getBinding("confirmationBehavior"):map(function(value)
|
||||
return value == "Large Changes"
|
||||
end),
|
||||
input = e(TextInput, {
|
||||
size = UDim2.new(0, 40, 0, 28),
|
||||
text = Settings:getBinding("largeChangesConfirmationThreshold"):map(function(value)
|
||||
return tostring(value)
|
||||
end),
|
||||
input = e(TextInput, {
|
||||
size = UDim2.new(0, 40, 0, 28),
|
||||
text = Settings:getBinding("largeChangesConfirmationThreshold"):map(function(value)
|
||||
return tostring(value)
|
||||
end),
|
||||
transparency = self.props.transparency,
|
||||
enabled = true,
|
||||
onEntered = function(text)
|
||||
local number = tonumber(string.match(text, "%d+"))
|
||||
if number then
|
||||
Settings:set("largeChangesConfirmationThreshold", math.clamp(number, 1, 999))
|
||||
else
|
||||
-- Force text back to last valid value
|
||||
Settings:set(
|
||||
"largeChangesConfirmationThreshold",
|
||||
Settings:get("largeChangesConfirmationThreshold")
|
||||
)
|
||||
end
|
||||
end,
|
||||
}),
|
||||
}),
|
||||
|
||||
PlaySounds = e(Setting, {
|
||||
id = "playSounds",
|
||||
name = "Play Sounds",
|
||||
description = "Toggle sound effects",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
CheckForUpdates = e(Setting, {
|
||||
id = "checkForUpdates",
|
||||
name = "Check For Updates",
|
||||
description = "Notify about newer compatible Rojo releases",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
CheckForPreleases = e(Setting, {
|
||||
id = "checkForPrereleases",
|
||||
name = "Include Prerelease Updates",
|
||||
description = "Include prereleases when checking for updates",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
visible = if string.find(debug.traceback(), "\n[^\n]-user_.-$") == nil
|
||||
then false -- Must be a local install to allow prerelease checks
|
||||
else Settings:getBinding("checkForUpdates"),
|
||||
}),
|
||||
|
||||
AutoConnectPlaytestServer = e(Setting, {
|
||||
id = "autoConnectPlaytestServer",
|
||||
name = "Auto Connect Playtest Server",
|
||||
description = "Automatically connect game server to Rojo when playtesting while connected in Edit",
|
||||
tag = "unstable",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
OpenScriptsExternally = e(Setting, {
|
||||
id = "openScriptsExternally",
|
||||
name = "Open Scripts Externally",
|
||||
description = "Attempt to open scripts in an external editor",
|
||||
tag = "unstable",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
TwoWaySync = e(Setting, {
|
||||
id = "twoWaySync",
|
||||
name = "Two-Way Sync",
|
||||
description = "Editing files in Studio will sync them into the filesystem",
|
||||
locked = self.props.syncActive,
|
||||
tag = "unstable",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
LogLevel = e(Setting, {
|
||||
id = "logLevel",
|
||||
name = "Log Level",
|
||||
description = "Plugin output verbosity level",
|
||||
tag = "debug",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
|
||||
options = invertedLevels,
|
||||
showReset = Settings:getBinding("logLevel"):map(function(value)
|
||||
return value ~= "Info"
|
||||
end),
|
||||
onReset = function()
|
||||
Settings:set("logLevel", "Info")
|
||||
enabled = true,
|
||||
onEntered = function(text)
|
||||
local number = tonumber(string.match(text, "%d+"))
|
||||
if number then
|
||||
Settings:set("largeChangesConfirmationThreshold", math.clamp(number, 1, 999))
|
||||
else
|
||||
-- Force text back to last valid value
|
||||
Settings:set(
|
||||
"largeChangesConfirmationThreshold",
|
||||
Settings:get("largeChangesConfirmationThreshold")
|
||||
)
|
||||
end
|
||||
end,
|
||||
}),
|
||||
|
||||
TypecheckingEnabled = e(Setting, {
|
||||
id = "typecheckingEnabled",
|
||||
name = "Typechecking",
|
||||
description = "Toggle typechecking on the API surface",
|
||||
tag = "debug",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
TimingLogsEnabled = e(Setting, {
|
||||
id = "timingLogsEnabled",
|
||||
name = "Timing Logs",
|
||||
description = "Toggle logging timing of internal actions for benchmarking Rojo performance",
|
||||
tag = "debug",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
|
||||
[Roact.Change.AbsoluteContentSize] = function(object)
|
||||
self.setContentSize(object.AbsoluteContentSize)
|
||||
end,
|
||||
}),
|
||||
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 20),
|
||||
PaddingRight = UDim.new(0, 20),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
end)
|
||||
|
||||
PlaySounds = e(Setting, {
|
||||
id = "playSounds",
|
||||
name = "Play Sounds",
|
||||
description = "Toggle sound effects",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
EnableSyncFallback = e(Setting, {
|
||||
id = "enableSyncFallback",
|
||||
name = "Enable Sync Fallback",
|
||||
description = "Whether Instances that fail to sync are remade as a fallback. If this is enabled, Instances may be destroyed and remade when syncing.",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
CheckForUpdates = e(Setting, {
|
||||
id = "checkForUpdates",
|
||||
name = "Check For Updates",
|
||||
description = "Notify about newer compatible Rojo releases",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
CheckForPreleases = e(Setting, {
|
||||
id = "checkForPrereleases",
|
||||
name = "Include Prerelease Updates",
|
||||
description = "Include prereleases when checking for updates",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
visible = if string.find(debug.traceback(), "\n[^\n]-user_.-$") == nil
|
||||
then false -- Must be a local install to allow prerelease checks
|
||||
else Settings:getBinding("checkForUpdates"),
|
||||
}),
|
||||
|
||||
AutoConnectPlaytestServer = e(Setting, {
|
||||
id = "autoConnectPlaytestServer",
|
||||
name = "Auto Connect Playtest Server",
|
||||
description = "Automatically connect game server to Rojo when playtesting while connected in Edit",
|
||||
tag = "unstable",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
OpenScriptsExternally = e(Setting, {
|
||||
id = "openScriptsExternally",
|
||||
name = "Open Scripts Externally",
|
||||
description = "Attempt to open scripts in an external editor",
|
||||
tag = "unstable",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
TwoWaySync = e(Setting, {
|
||||
id = "twoWaySync",
|
||||
name = "Two-Way Sync",
|
||||
description = "Editing files in Studio will sync them into the filesystem",
|
||||
locked = self.props.syncActive,
|
||||
lockedTooltip = "(Cannot change while currently syncing. Disconnect first.)",
|
||||
tag = "unstable",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
LogLevel = e(Setting, {
|
||||
id = "logLevel",
|
||||
name = "Log Level",
|
||||
description = "Plugin output verbosity level",
|
||||
tag = "debug",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
|
||||
options = invertedLevels,
|
||||
showReset = Settings:getBinding("logLevel"):map(function(value)
|
||||
return value ~= "Info"
|
||||
end),
|
||||
onReset = function()
|
||||
Settings:set("logLevel", "Info")
|
||||
end,
|
||||
}),
|
||||
|
||||
TypecheckingEnabled = e(Setting, {
|
||||
id = "typecheckingEnabled",
|
||||
name = "Typechecking",
|
||||
description = "Toggle typechecking on the API surface",
|
||||
tag = "debug",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
TimingLogsEnabled = e(Setting, {
|
||||
id = "timingLogsEnabled",
|
||||
name = "Timing Logs",
|
||||
description = "Toggle logging timing of internal actions for benchmarking Rojo performance",
|
||||
tag = "debug",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
|
||||
[Roact.Change.AbsoluteContentSize] = function(object)
|
||||
self.setContentSize(object.AbsoluteContentSize)
|
||||
end,
|
||||
}),
|
||||
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 20),
|
||||
PaddingRight = UDim.new(0, 20),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return SettingsPage
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
--[[
|
||||
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.
|
||||
Theming system provided through Roact's context.
|
||||
Uses Studio colors when possible.
|
||||
]]
|
||||
|
||||
-- Studio does not exist outside Roblox Studio, so we'll lazily initialize it
|
||||
@@ -15,6 +14,8 @@ local function getStudio()
|
||||
return _Studio
|
||||
end
|
||||
|
||||
local ContentProvider = game:GetService("ContentProvider")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
@@ -35,6 +36,27 @@ function StudioProvider:updateTheme()
|
||||
local isDark = studioTheme.Name == "Dark"
|
||||
|
||||
local theme = strict(studioTheme.Name .. "Theme", {
|
||||
Font = {
|
||||
Main = Font.new("rbxasset://fonts/families/Montserrat.json", Enum.FontWeight.Medium, Enum.FontStyle.Normal),
|
||||
Bold = Font.new("rbxasset://fonts/families/Montserrat.json", Enum.FontWeight.Bold, Enum.FontStyle.Normal),
|
||||
Thin = Font.new(
|
||||
"rbxasset://fonts/families/Montserrat.json",
|
||||
Enum.FontWeight.Regular,
|
||||
Enum.FontStyle.Normal
|
||||
),
|
||||
Code = Font.new(
|
||||
"rbxasset://fonts/families/Inconsolata.json",
|
||||
Enum.FontWeight.Regular,
|
||||
Enum.FontStyle.Normal
|
||||
),
|
||||
},
|
||||
TextSize = {
|
||||
Body = 15,
|
||||
Small = 13,
|
||||
Medium = 16,
|
||||
Large = 18,
|
||||
Code = 16,
|
||||
},
|
||||
BrandColor = BRAND_COLOR,
|
||||
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
|
||||
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
|
||||
@@ -143,12 +165,29 @@ function StudioProvider:updateTheme()
|
||||
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
|
||||
},
|
||||
Diff = {
|
||||
-- Studio doesn't have good colors since their diffs use backgrounds, not text
|
||||
Add = if isDark then Color3.fromRGB(143, 227, 154) else Color3.fromRGB(41, 164, 45),
|
||||
Remove = if isDark then Color3.fromRGB(242, 125, 125) else Color3.fromRGB(150, 29, 29),
|
||||
Edit = if isDark then Color3.fromRGB(120, 154, 248) else Color3.fromRGB(0, 70, 160),
|
||||
-- Very bright different colors in case some places were not updated to use
|
||||
-- the new background diff colors.
|
||||
Add = Color3.fromRGB(255, 0, 255),
|
||||
Remove = Color3.fromRGB(255, 0, 255),
|
||||
Edit = Color3.fromRGB(255, 0, 255),
|
||||
|
||||
Row = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||
Warning = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText),
|
||||
|
||||
Background = {
|
||||
-- Studio doesn't have good colors since their diffs use backgrounds, not text
|
||||
Add = if isDark then Color3.fromRGB(143, 227, 154) else Color3.fromRGB(41, 164, 45),
|
||||
Remove = if isDark then Color3.fromRGB(242, 125, 125) else Color3.fromRGB(150, 29, 29),
|
||||
Edit = if isDark then Color3.fromRGB(120, 154, 248) else Color3.fromRGB(0, 70, 160),
|
||||
Remain = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||
},
|
||||
|
||||
Text = {
|
||||
Add = if isDark then Color3.new(0, 0, 0) else Color3.new(1, 1, 1),
|
||||
Remove = if isDark then Color3.new(0, 0, 0) else Color3.new(1, 1, 1),
|
||||
Edit = if isDark then Color3.new(0, 0, 0) else Color3.new(1, 1, 1),
|
||||
Remain = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
|
||||
},
|
||||
},
|
||||
ConnectionDetails = {
|
||||
ProjectNameColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||
@@ -190,6 +229,13 @@ end
|
||||
|
||||
function StudioProvider:init()
|
||||
self:updateTheme()
|
||||
|
||||
-- Preload the Fonts so that getTextBoundsAsync won't yield
|
||||
local fontAssetIds = {}
|
||||
for _, font in self.state.theme.Font do
|
||||
table.insert(fontAssetIds, font.Family)
|
||||
end
|
||||
pcall(ContentProvider.PreloadAsync, ContentProvider, fontAssetIds)
|
||||
end
|
||||
|
||||
function StudioProvider:render()
|
||||
|
||||
41
plugin/src/App/getTextBoundsAsync.lua
Normal file
41
plugin/src/App/getTextBoundsAsync.lua
Normal file
@@ -0,0 +1,41 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local params = Instance.new("GetTextBoundsParams")
|
||||
|
||||
local function getTextBoundsAsync(
|
||||
text: string,
|
||||
font: Font,
|
||||
textSize: number,
|
||||
width: number,
|
||||
richText: boolean?
|
||||
): Vector2
|
||||
if type(text) ~= "string" then
|
||||
Log.warn(`Invalid text. Expected string, received {type(text)} instead`)
|
||||
return Vector2.zero
|
||||
end
|
||||
if #text >= 200_000 then
|
||||
Log.warn(`Invalid text. Exceeds the 199,999 character limit`)
|
||||
return Vector2.zero
|
||||
end
|
||||
|
||||
params.Text = text
|
||||
params.Font = font
|
||||
params.Size = textSize
|
||||
params.Width = width
|
||||
params.RichText = not not richText
|
||||
|
||||
local success, bounds = pcall(TextService.GetTextBoundsAsync, TextService, params)
|
||||
if not success then
|
||||
Log.warn(`Failed to get text bounds: {bounds}`)
|
||||
return Vector2.zero
|
||||
end
|
||||
|
||||
return bounds
|
||||
end
|
||||
|
||||
return getTextBoundsAsync
|
||||
@@ -9,6 +9,7 @@ local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
local Log = require(Packages.Log)
|
||||
local Promise = require(Packages.Promise)
|
||||
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Version = require(Plugin.Version)
|
||||
@@ -27,7 +28,7 @@ local timeUtil = require(Plugin.timeUtil)
|
||||
local Theme = require(script.Theme)
|
||||
|
||||
local Page = require(script.Page)
|
||||
local Notifications = require(script.Notifications)
|
||||
local Notifications = require(script.Components.Notifications)
|
||||
local Tooltip = require(script.Components.Tooltip)
|
||||
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
|
||||
local StudioToolbar = require(script.Components.Studio.StudioToolbar)
|
||||
@@ -52,9 +53,9 @@ local App = Roact.Component:extend("App")
|
||||
function App:init()
|
||||
preloadAssets()
|
||||
|
||||
local priorHost, priorPort = self:getPriorEndpoint()
|
||||
self.host, self.setHost = Roact.createBinding(priorHost or "")
|
||||
self.port, self.setPort = Roact.createBinding(priorPort or "")
|
||||
local priorSyncInfo = self:getPriorSyncInfo()
|
||||
self.host, self.setHost = Roact.createBinding(priorSyncInfo.host or "")
|
||||
self.port, self.setPort = Roact.createBinding(priorSyncInfo.port or "")
|
||||
|
||||
self.confirmationBindable = Instance.new("BindableEvent")
|
||||
self.confirmationEvent = self.confirmationBindable.Event
|
||||
@@ -78,17 +79,18 @@ function App:init()
|
||||
action
|
||||
)
|
||||
)
|
||||
local dismissNotif = self:addNotification(
|
||||
string.format("You've undone '%s'.\nIf this was not intended, please restore.", action),
|
||||
10,
|
||||
{
|
||||
local dismissNotif = self:addNotification({
|
||||
text = string.format("You've undone '%s'.\nIf this was not intended, please restore.", action),
|
||||
timeout = 10,
|
||||
onClose = function()
|
||||
cleanup()
|
||||
end,
|
||||
actions = {
|
||||
Restore = {
|
||||
text = "Restore",
|
||||
style = "Solid",
|
||||
layoutOrder = 1,
|
||||
onClick = function(notification)
|
||||
cleanup()
|
||||
notification:dismiss()
|
||||
onClick = function()
|
||||
ChangeHistoryService:Redo()
|
||||
end,
|
||||
},
|
||||
@@ -96,13 +98,9 @@ function App:init()
|
||||
text = "Dismiss",
|
||||
style = "Bordered",
|
||||
layoutOrder = 2,
|
||||
onClick = function(notification)
|
||||
cleanup()
|
||||
notification:dismiss()
|
||||
end,
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
undoConnection = ChangeHistoryService.OnUndo:Once(function()
|
||||
-- Our notif is now out of date- redoing will not restore the patch
|
||||
@@ -142,32 +140,20 @@ function App:init()
|
||||
if RunService:IsEdit() then
|
||||
self:checkForUpdates()
|
||||
|
||||
if
|
||||
Settings:get("syncReminder")
|
||||
and self.serveSession == nil
|
||||
and self:getLastSyncTimestamp()
|
||||
and (self:isSyncLockAvailable())
|
||||
then
|
||||
self:addNotification("You've previously synced this place. Would you like to reconnect?", 300, {
|
||||
Connect = {
|
||||
text = "Connect",
|
||||
style = "Solid",
|
||||
layoutOrder = 1,
|
||||
onClick = function(notification)
|
||||
notification:dismiss()
|
||||
self:startSession()
|
||||
end,
|
||||
},
|
||||
Dismiss = {
|
||||
text = "Dismiss",
|
||||
style = "Bordered",
|
||||
layoutOrder = 2,
|
||||
onClick = function(notification)
|
||||
notification:dismiss()
|
||||
end,
|
||||
},
|
||||
})
|
||||
end
|
||||
self:startSyncReminderPolling()
|
||||
self.disconnectSyncReminderPollingChanged = Settings:onChanged("syncReminderPolling", function(enabled)
|
||||
if enabled then
|
||||
self:startSyncReminderPolling()
|
||||
else
|
||||
self:stopSyncReminderPolling()
|
||||
end
|
||||
end)
|
||||
|
||||
self:tryAutoReconnect():andThen(function(didReconnect)
|
||||
if not didReconnect then
|
||||
self:checkSyncReminder()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if self:isAutoConnectPlaytestServerAvailable() then
|
||||
@@ -193,16 +179,23 @@ function App:willUnmount()
|
||||
|
||||
self.disconnectUpdatesCheckChanged()
|
||||
self.disconnectPrereleasesCheckChanged()
|
||||
if self.disconnectSyncReminderPollingChanged then
|
||||
self.disconnectSyncReminderPollingChanged()
|
||||
end
|
||||
|
||||
self:stopSyncReminderPolling()
|
||||
|
||||
self.autoConnectPlaytestServerListener()
|
||||
self:clearRunningConnectionInfo()
|
||||
end
|
||||
|
||||
function App:addNotification(
|
||||
function App:addNotification(notif: {
|
||||
text: string,
|
||||
isFullscreen: boolean?,
|
||||
timeout: number?,
|
||||
actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> () } }?
|
||||
)
|
||||
actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> ()? } }?,
|
||||
onClose: (any) -> ()?,
|
||||
})
|
||||
if not Settings:get("showNotifications") then
|
||||
return
|
||||
end
|
||||
@@ -210,17 +203,17 @@ function App:addNotification(
|
||||
self.notifId += 1
|
||||
local id = self.notifId
|
||||
|
||||
local notifications = table.clone(self.state.notifications)
|
||||
notifications[id] = {
|
||||
text = text,
|
||||
timestamp = DateTime.now().UnixTimestampMillis,
|
||||
timeout = timeout or 3,
|
||||
actions = actions,
|
||||
}
|
||||
self:setState(function(prevState)
|
||||
local notifications = table.clone(prevState.notifications)
|
||||
notifications[id] = Dictionary.merge({
|
||||
timeout = notif.timeout or 5,
|
||||
isFullscreen = notif.isFullscreen or false,
|
||||
}, notif)
|
||||
|
||||
self:setState({
|
||||
notifications = notifications,
|
||||
})
|
||||
return {
|
||||
notifications = notifications,
|
||||
}
|
||||
end)
|
||||
|
||||
return function()
|
||||
self:closeNotification(id)
|
||||
@@ -232,96 +225,60 @@ function App:closeNotification(id: number)
|
||||
return
|
||||
end
|
||||
|
||||
local notifications = table.clone(self.state.notifications)
|
||||
notifications[id] = nil
|
||||
self:setState(function(prevState)
|
||||
local notifications = table.clone(prevState.notifications)
|
||||
notifications[id] = nil
|
||||
|
||||
self:setState({
|
||||
notifications = notifications,
|
||||
})
|
||||
return {
|
||||
notifications = notifications,
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
function App:checkForUpdates()
|
||||
if not Settings:get("checkForUpdates") then
|
||||
return
|
||||
end
|
||||
local updateMessage = Version.getUpdateMessage()
|
||||
|
||||
local isLocalInstall = string.find(debug.traceback(), "\n[^\n]-user_.-$") ~= nil
|
||||
local latestCompatibleVersion = Version.retrieveLatestCompatible({
|
||||
version = Config.version,
|
||||
includePrereleases = isLocalInstall and Settings:get("checkForPrereleases"),
|
||||
})
|
||||
if not latestCompatibleVersion then
|
||||
return
|
||||
end
|
||||
|
||||
self:addNotification(
|
||||
string.format(
|
||||
"A newer compatible version of Rojo, %s, was published %s! Go to the Rojo releases page to learn more.",
|
||||
Version.display(latestCompatibleVersion.version),
|
||||
timeUtil.elapsedToText(DateTime.now().UnixTimestamp - latestCompatibleVersion.publishedUnixTimestamp)
|
||||
),
|
||||
500,
|
||||
{
|
||||
Dismiss = {
|
||||
text = "Dismiss",
|
||||
style = "Bordered",
|
||||
layoutOrder = 2,
|
||||
onClick = function(notification)
|
||||
notification:dismiss()
|
||||
end,
|
||||
if updateMessage then
|
||||
self:addNotification({
|
||||
text = updateMessage,
|
||||
timeout = 500,
|
||||
actions = {
|
||||
Dismiss = {
|
||||
text = "Dismiss",
|
||||
style = "Bordered",
|
||||
layoutOrder = 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function App:getPriorEndpoint()
|
||||
local priorEndpoints = Settings:get("priorEndpoints")
|
||||
if not priorEndpoints then
|
||||
return
|
||||
function App:getPriorSyncInfo(): { host: string?, port: string?, projectName: string?, timestamp: number? }
|
||||
local priorSyncInfos = Settings:get("priorEndpoints")
|
||||
if not priorSyncInfos then
|
||||
return {}
|
||||
end
|
||||
|
||||
local id = tostring(game.PlaceId)
|
||||
if ignorePlaceIds[id] then
|
||||
return
|
||||
return {}
|
||||
end
|
||||
|
||||
local place = priorEndpoints[id]
|
||||
if not place then
|
||||
return
|
||||
end
|
||||
|
||||
return place.host, place.port
|
||||
return priorSyncInfos[id] or {}
|
||||
end
|
||||
|
||||
function App:getLastSyncTimestamp()
|
||||
local priorEndpoints = Settings:get("priorEndpoints")
|
||||
if not priorEndpoints then
|
||||
return
|
||||
function App:setPriorSyncInfo(host: string, port: string, projectName: string)
|
||||
local priorSyncInfos = Settings:get("priorEndpoints")
|
||||
if not priorSyncInfos then
|
||||
priorSyncInfos = {}
|
||||
end
|
||||
|
||||
local id = tostring(game.PlaceId)
|
||||
if ignorePlaceIds[id] then
|
||||
return
|
||||
end
|
||||
|
||||
local place = priorEndpoints[id]
|
||||
if not place then
|
||||
return
|
||||
end
|
||||
|
||||
return place.timestamp
|
||||
end
|
||||
|
||||
function App:setPriorEndpoint(host: string, port: string)
|
||||
local priorEndpoints = Settings:get("priorEndpoints")
|
||||
if not priorEndpoints then
|
||||
priorEndpoints = {}
|
||||
end
|
||||
local now = os.time()
|
||||
|
||||
-- Clear any stale saves to avoid disc bloat
|
||||
for placeId, endpoint in priorEndpoints do
|
||||
if os.time() - endpoint.timestamp > 12_960_000 then
|
||||
priorEndpoints[placeId] = nil
|
||||
for placeId, syncInfo in priorSyncInfos do
|
||||
if now - (syncInfo.timestamp or now) > 12_960_000 then
|
||||
priorSyncInfos[placeId] = nil
|
||||
Log.trace("Cleared stale saved endpoint for {}", placeId)
|
||||
end
|
||||
end
|
||||
@@ -331,14 +288,15 @@ function App:setPriorEndpoint(host: string, port: string)
|
||||
return
|
||||
end
|
||||
|
||||
priorEndpoints[id] = {
|
||||
priorSyncInfos[id] = {
|
||||
host = if host ~= Config.defaultHost then host else nil,
|
||||
port = if port ~= Config.defaultPort then port else nil,
|
||||
timestamp = os.time(),
|
||||
projectName = projectName,
|
||||
timestamp = now,
|
||||
}
|
||||
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
||||
|
||||
Settings:set("priorEndpoints", priorEndpoints)
|
||||
Settings:set("priorEndpoints", priorSyncInfos)
|
||||
end
|
||||
|
||||
function App:getHostAndPort()
|
||||
@@ -413,8 +371,158 @@ function App:releaseSyncLock()
|
||||
Log.trace("Could not relase sync lock because it is owned by {}", lock.Value)
|
||||
end
|
||||
|
||||
function App:findActiveServer()
|
||||
local host, port = self:getHostAndPort()
|
||||
local baseUrl = if string.find(host, "^https?://")
|
||||
then string.format("%s:%s", host, port)
|
||||
else string.format("http://%s:%s", host, port)
|
||||
|
||||
Log.trace("Checking for active sync server at {}", baseUrl)
|
||||
|
||||
local apiContext = ApiContext.new(baseUrl)
|
||||
return apiContext:connect():andThen(function(serverInfo)
|
||||
apiContext:disconnect()
|
||||
return serverInfo, host, port
|
||||
end)
|
||||
end
|
||||
|
||||
function App:tryAutoReconnect()
|
||||
if not Settings:get("autoReconnect") then
|
||||
return Promise.resolve(false)
|
||||
end
|
||||
|
||||
local priorSyncInfo = self:getPriorSyncInfo()
|
||||
if not priorSyncInfo.projectName then
|
||||
Log.trace("No prior sync info found, skipping auto-reconnect")
|
||||
return Promise.resolve(false)
|
||||
end
|
||||
|
||||
return self:findActiveServer()
|
||||
:andThen(function(serverInfo)
|
||||
-- change
|
||||
if serverInfo.projectName == priorSyncInfo.projectName then
|
||||
Log.trace("Auto-reconnect found matching server, reconnecting...")
|
||||
self:addNotification({
|
||||
text = `Auto-reconnect discovered project '{serverInfo.projectName}'...`,
|
||||
})
|
||||
self:startSession()
|
||||
return true
|
||||
end
|
||||
Log.trace("Auto-reconnect found different server, not reconnecting")
|
||||
return false
|
||||
end)
|
||||
:catch(function()
|
||||
Log.trace("Auto-reconnect did not find a server, not reconnecting")
|
||||
return false
|
||||
end)
|
||||
end
|
||||
|
||||
function App:checkSyncReminder()
|
||||
local syncReminderMode = Settings:get("syncReminderMode")
|
||||
if syncReminderMode == "None" then
|
||||
return
|
||||
end
|
||||
|
||||
if self.serveSession ~= nil or not self:isSyncLockAvailable() then
|
||||
-- Already syncing or cannot sync, no reason to remind
|
||||
return
|
||||
end
|
||||
|
||||
local priorSyncInfo = self:getPriorSyncInfo()
|
||||
|
||||
self:findActiveServer()
|
||||
:andThen(function(serverInfo, host, port)
|
||||
self:sendSyncReminder(
|
||||
`Project '{serverInfo.projectName}' is serving at {host}:{port}.\nWould you like to connect?`
|
||||
)
|
||||
end)
|
||||
:catch(function()
|
||||
if priorSyncInfo.timestamp and priorSyncInfo.projectName then
|
||||
-- We didn't find an active server,
|
||||
-- but this place has a prior sync
|
||||
-- so we should remind the user to serve
|
||||
|
||||
local timeSinceSync = timeUtil.elapsedToText(os.time() - priorSyncInfo.timestamp)
|
||||
self:sendSyncReminder(
|
||||
`You synced project '{priorSyncInfo.projectName}' to this place {timeSinceSync}.\nDid you mean to run 'rojo serve' and then connect?`
|
||||
)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function App:startSyncReminderPolling()
|
||||
if
|
||||
self.syncReminderPollingThread ~= nil
|
||||
or Settings:get("syncReminderMode") == "None"
|
||||
or not Settings:get("syncReminderPolling")
|
||||
then
|
||||
return
|
||||
end
|
||||
|
||||
Log.trace("Starting sync reminder polling thread")
|
||||
self.syncReminderPollingThread = task.spawn(function()
|
||||
while task.wait(30) do
|
||||
if self.syncReminderPollingThread == nil then
|
||||
-- The polling thread was stopped, so exit
|
||||
return
|
||||
end
|
||||
if self.dismissSyncReminder then
|
||||
-- There is already a sync reminder being shown
|
||||
task.wait(5)
|
||||
continue
|
||||
end
|
||||
self:checkSyncReminder()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function App:stopSyncReminderPolling()
|
||||
if self.syncReminderPollingThread then
|
||||
Log.trace("Stopping sync reminder polling thread")
|
||||
task.cancel(self.syncReminderPollingThread)
|
||||
self.syncReminderPollingThread = nil
|
||||
end
|
||||
end
|
||||
|
||||
function App:sendSyncReminder(message: string)
|
||||
local syncReminderMode = Settings:get("syncReminderMode")
|
||||
if syncReminderMode == "None" then
|
||||
return
|
||||
end
|
||||
|
||||
self.dismissSyncReminder = self:addNotification({
|
||||
text = message,
|
||||
timeout = 120,
|
||||
isFullscreen = Settings:get("syncReminderMode") == "Fullscreen",
|
||||
onClose = function()
|
||||
self.dismissSyncReminder = nil
|
||||
end,
|
||||
actions = {
|
||||
Connect = {
|
||||
text = "Connect",
|
||||
style = "Solid",
|
||||
layoutOrder = 1,
|
||||
onClick = function()
|
||||
self:startSession()
|
||||
end,
|
||||
},
|
||||
Dismiss = {
|
||||
text = "Dismiss",
|
||||
style = "Bordered",
|
||||
layoutOrder = 2,
|
||||
onClick = function()
|
||||
-- If the user dismisses the reminder,
|
||||
-- then we don't need to remind them again
|
||||
self:stopSyncReminderPolling()
|
||||
end,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
function App:isAutoConnectPlaytestServerAvailable()
|
||||
return RunService:IsRunMode()
|
||||
return RunService:IsRunning()
|
||||
and RunService:IsStudio()
|
||||
and RunService:IsServer()
|
||||
and Settings:get("autoConnectPlaytestServer")
|
||||
and workspace:GetAttribute("__Rojo_ConnectionUrl")
|
||||
@@ -462,7 +570,10 @@ function App:startSession()
|
||||
local msg = string.format("Could not sync because user '%s' is already syncing", tostring(priorOwner))
|
||||
|
||||
Log.warn(msg)
|
||||
self:addNotification(msg, 10)
|
||||
self:addNotification({
|
||||
text = msg,
|
||||
timeout = 10,
|
||||
})
|
||||
self:setState({
|
||||
appStatus = AppStatus.Error,
|
||||
errorMessage = msg,
|
||||
@@ -484,64 +595,62 @@ function App:startSession()
|
||||
twoWaySync = Settings:get("twoWaySync"),
|
||||
})
|
||||
|
||||
self.cleanupPrecommit = serveSession.__reconciler:hookPrecommit(function(patch, instanceMap)
|
||||
serveSession:setUpdateLoadingTextCallback(function(text: string)
|
||||
self:setState({
|
||||
connectingText = text,
|
||||
})
|
||||
end)
|
||||
|
||||
self.cleanupPrecommit = serveSession:hookPrecommit(function(patch, instanceMap)
|
||||
-- Build new tree for patch
|
||||
self:setState({
|
||||
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Old", "New" }),
|
||||
})
|
||||
end)
|
||||
self.cleanupPostcommit = serveSession.__reconciler:hookPostcommit(function(patch, instanceMap, unappliedPatch)
|
||||
-- Update tree with unapplied metadata
|
||||
self.cleanupPostcommit = serveSession:hookPostcommit(function(patch, instanceMap, unappliedPatch)
|
||||
local now = DateTime.now().UnixTimestamp
|
||||
self:setState(function(prevState)
|
||||
local oldPatchData = prevState.patchData
|
||||
local newPatchData = {
|
||||
patch = patch,
|
||||
unapplied = unappliedPatch,
|
||||
timestamp = now,
|
||||
}
|
||||
|
||||
if PatchSet.isEmpty(patch) then
|
||||
-- Keep existing patch info, but use new timestamp
|
||||
newPatchData.patch = oldPatchData.patch
|
||||
newPatchData.unapplied = oldPatchData.unapplied
|
||||
elseif now - oldPatchData.timestamp < 2 then
|
||||
-- Patches that apply in the same second are combined for human clarity
|
||||
newPatchData.patch = PatchSet.assign(PatchSet.newEmpty(), oldPatchData.patch, patch)
|
||||
newPatchData.unapplied = PatchSet.assign(PatchSet.newEmpty(), oldPatchData.unapplied, unappliedPatch)
|
||||
end
|
||||
|
||||
return {
|
||||
patchTree = PatchTree.updateMetadata(prevState.patchTree, patch, instanceMap, unappliedPatch),
|
||||
patchData = newPatchData,
|
||||
}
|
||||
end)
|
||||
end)
|
||||
|
||||
serveSession:hookPostcommit(function(patch, _instanceMap, unapplied)
|
||||
local now = DateTime.now().UnixTimestamp
|
||||
local old = self.state.patchData
|
||||
|
||||
if PatchSet.isEmpty(patch) then
|
||||
-- Ignore empty patch, but update timestamp
|
||||
self:setState({
|
||||
patchData = {
|
||||
patch = old.patch,
|
||||
unapplied = old.unapplied,
|
||||
timestamp = now,
|
||||
},
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
if now - old.timestamp < 2 then
|
||||
-- Patches that apply in the same second are
|
||||
-- considered to be part of the same change for human clarity
|
||||
patch = PatchSet.assign(PatchSet.newEmpty(), old.patch, patch)
|
||||
unapplied = PatchSet.assign(PatchSet.newEmpty(), old.unapplied, unapplied)
|
||||
end
|
||||
|
||||
self:setState({
|
||||
patchData = {
|
||||
patch = patch,
|
||||
unapplied = unapplied,
|
||||
timestamp = now,
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
serveSession:onStatusChanged(function(status, details)
|
||||
if status == ServeSession.Status.Connecting then
|
||||
self:setPriorEndpoint(host, port)
|
||||
if self.dismissSyncReminder then
|
||||
self.dismissSyncReminder()
|
||||
self.dismissSyncReminder = nil
|
||||
end
|
||||
|
||||
self:setState({
|
||||
appStatus = AppStatus.Connecting,
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
})
|
||||
self:addNotification("Connecting to session...")
|
||||
self:addNotification({
|
||||
text = "Connecting to session...",
|
||||
})
|
||||
elseif status == ServeSession.Status.Connected then
|
||||
self.knownProjects[details] = true
|
||||
self:setPriorSyncInfo(host, port, details)
|
||||
self:setRunningConnectionInfo(baseUrl)
|
||||
|
||||
local address = ("%s:%s"):format(host, port)
|
||||
@@ -551,7 +660,9 @@ function App:startSession()
|
||||
address = address,
|
||||
toolbarIcon = Assets.Images.PluginButtonConnected,
|
||||
})
|
||||
self:addNotification(string.format("Connected to session '%s' at %s.", details, address), 5)
|
||||
self:addNotification({
|
||||
text = string.format("Connected to session '%s' at %s.", details, address),
|
||||
})
|
||||
elseif status == ServeSession.Status.Disconnected then
|
||||
self.serveSession = nil
|
||||
self:releaseSyncLock()
|
||||
@@ -574,13 +685,19 @@ function App:startSession()
|
||||
errorMessage = tostring(details),
|
||||
toolbarIcon = Assets.Images.PluginButtonWarning,
|
||||
})
|
||||
self:addNotification(tostring(details), 10)
|
||||
self:addNotification({
|
||||
text = tostring(details),
|
||||
timeout = 10,
|
||||
})
|
||||
else
|
||||
self:setState({
|
||||
appStatus = AppStatus.NotConnected,
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
})
|
||||
self:addNotification("Disconnected from session.")
|
||||
self:addNotification({
|
||||
text = "Disconnected from session.",
|
||||
timeout = 10,
|
||||
})
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -648,23 +765,25 @@ function App:startSession()
|
||||
end
|
||||
end
|
||||
|
||||
self:setState({
|
||||
connectingText = "Computing diff view...",
|
||||
})
|
||||
self:setState({
|
||||
appStatus = AppStatus.Confirming,
|
||||
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }),
|
||||
confirmData = {
|
||||
instanceMap = instanceMap,
|
||||
patch = patch,
|
||||
serverInfo = serverInfo,
|
||||
},
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
})
|
||||
|
||||
self:addNotification(
|
||||
string.format(
|
||||
self:addNotification({
|
||||
text = string.format(
|
||||
"Please accept%sor abort the initializing sync session.",
|
||||
Settings:get("twoWaySync") and ", reject, " or " "
|
||||
),
|
||||
7
|
||||
)
|
||||
timeout = 7,
|
||||
})
|
||||
|
||||
return self.confirmationEvent:Wait()
|
||||
end)
|
||||
@@ -763,6 +882,7 @@ function App:render()
|
||||
|
||||
ConfirmingPage = createPageElement(AppStatus.Confirming, {
|
||||
confirmData = self.state.confirmData,
|
||||
patchTree = self.state.patchTree,
|
||||
createPopup = not self.state.guiEnabled,
|
||||
|
||||
onAbort = function()
|
||||
@@ -776,7 +896,9 @@ function App:render()
|
||||
end,
|
||||
}),
|
||||
|
||||
Connecting = createPageElement(AppStatus.Connecting),
|
||||
Connecting = createPageElement(AppStatus.Connecting, {
|
||||
text = self.state.connectingText,
|
||||
}),
|
||||
|
||||
Connected = createPageElement(AppStatus.Connected, {
|
||||
projectName = self.state.projectName,
|
||||
@@ -825,19 +947,7 @@ function App:render()
|
||||
ResetOnSpawn = false,
|
||||
DisplayOrder = 100,
|
||||
}, {
|
||||
layout = e("UIListLayout", {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Bottom,
|
||||
Padding = UDim.new(0, 5),
|
||||
}),
|
||||
padding = e("UIPadding", {
|
||||
PaddingTop = UDim.new(0, 5),
|
||||
PaddingBottom = UDim.new(0, 5),
|
||||
PaddingLeft = UDim.new(0, 5),
|
||||
PaddingRight = UDim.new(0, 5),
|
||||
}),
|
||||
notifs = e(Notifications, {
|
||||
Notifications = e(Notifications, {
|
||||
soundPlayer = self.props.soundPlayer,
|
||||
notifications = self.state.notifications,
|
||||
onClose = function(id)
|
||||
|
||||
@@ -282,6 +282,22 @@ function PatchSet.assign(target, ...)
|
||||
return target
|
||||
end
|
||||
|
||||
function PatchSet.addedIdList(patchSet): { string }
|
||||
local idList = table.create(#patchSet.added)
|
||||
for id in patchSet.added do
|
||||
table.insert(idList, id)
|
||||
end
|
||||
return table.freeze(idList)
|
||||
end
|
||||
|
||||
function PatchSet.updatedIdList(patchSet): { string }
|
||||
local idList = table.create(#patchSet.updated)
|
||||
for _, item in patchSet.updated do
|
||||
table.insert(idList, item.id)
|
||||
end
|
||||
return table.freeze(idList)
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a list of human-readable statements summarizing the contents of this
|
||||
patch, intended to be displayed to users.
|
||||
|
||||
@@ -16,6 +16,14 @@ local Types = require(Plugin.Types)
|
||||
local decodeValue = require(Plugin.Reconciler.decodeValue)
|
||||
local getProperty = require(Plugin.Reconciler.getProperty)
|
||||
|
||||
local function yieldIfNeeded(clock)
|
||||
if os.clock() - clock > 1 / 20 then
|
||||
task.wait()
|
||||
return os.clock()
|
||||
end
|
||||
return clock
|
||||
end
|
||||
|
||||
local function alphabeticalNext(t, state)
|
||||
-- Equivalent of the next function, but returns the keys in the alphabetic
|
||||
-- order of node names. We use a temporary ordered key table that is stored in the
|
||||
@@ -132,7 +140,6 @@ end
|
||||
-- props must contain id, and cannot contain children or parentId
|
||||
-- other than those three, it can hold anything
|
||||
function Tree:addNode(parent, props)
|
||||
Timer.start("Tree:addNode")
|
||||
assert(props.id, "props must contain id")
|
||||
|
||||
parent = parent or "ROOT"
|
||||
@@ -143,7 +150,6 @@ function Tree:addNode(parent, props)
|
||||
for k, v in props do
|
||||
node[k] = v
|
||||
end
|
||||
Timer.stop()
|
||||
return node
|
||||
end
|
||||
|
||||
@@ -154,25 +160,25 @@ function Tree:addNode(parent, props)
|
||||
local parentNode = self:getNode(parent)
|
||||
if not parentNode then
|
||||
Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
|
||||
Timer.stop()
|
||||
return
|
||||
end
|
||||
|
||||
parentNode.children[node.id] = node
|
||||
self.idToNode[node.id] = node
|
||||
|
||||
Timer.stop()
|
||||
return node
|
||||
end
|
||||
|
||||
-- Given a list of ancestor ids in descending order, builds the nodes for them
|
||||
-- using the patch and instanceMap info
|
||||
function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, patch, instanceMap)
|
||||
Timer.start("Tree:buildAncestryNodes")
|
||||
local clock = os.clock()
|
||||
-- Build nodes for ancestry by going up the tree
|
||||
previousId = previousId or "ROOT"
|
||||
|
||||
for _, ancestorId in ancestryIds do
|
||||
clock = yieldIfNeeded(clock)
|
||||
|
||||
local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
|
||||
if not value then
|
||||
Log.warn("Failed to find ancestor object for " .. ancestorId)
|
||||
@@ -186,8 +192,6 @@ function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, p
|
||||
})
|
||||
previousId = ancestorId
|
||||
end
|
||||
|
||||
Timer.stop()
|
||||
end
|
||||
|
||||
local PatchTree = {}
|
||||
@@ -196,12 +200,16 @@ local PatchTree = {}
|
||||
-- uses changeListHeaders in node.changeList
|
||||
function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||
Timer.start("PatchTree.build")
|
||||
local clock = os.clock()
|
||||
|
||||
local tree = Tree.new()
|
||||
|
||||
local knownAncestors = {}
|
||||
|
||||
Timer.start("patch.updated")
|
||||
for _, change in patch.updated do
|
||||
clock = yieldIfNeeded(clock)
|
||||
|
||||
local instance = instanceMap.fromIds[change.id]
|
||||
if not instance then
|
||||
continue
|
||||
@@ -281,6 +289,8 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||
|
||||
Timer.start("patch.removed")
|
||||
for _, idOrInstance in patch.removed do
|
||||
clock = yieldIfNeeded(clock)
|
||||
|
||||
local instance = if Types.RbxId(idOrInstance) then instanceMap.fromIds[idOrInstance] else idOrInstance
|
||||
if not instance then
|
||||
-- If we're viewing a past patch, the instance is already removed
|
||||
@@ -325,6 +335,8 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||
|
||||
Timer.start("patch.added")
|
||||
for id, change in patch.added do
|
||||
clock = yieldIfNeeded(clock)
|
||||
|
||||
-- Gather ancestors from existing DOM or future additions
|
||||
local ancestryIds = {}
|
||||
local parentId = change.Parent
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
Patches can come from the server or be generated by the client.
|
||||
]]
|
||||
|
||||
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
||||
|
||||
local Packages = script.Parent.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
@@ -16,19 +14,18 @@ local invariant = require(script.Parent.Parent.invariant)
|
||||
|
||||
local decodeValue = require(script.Parent.decodeValue)
|
||||
local reify = require(script.Parent.reify)
|
||||
local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferredRefs
|
||||
local setProperty = require(script.Parent.setProperty)
|
||||
|
||||
local function applyPatch(instanceMap, patch)
|
||||
local patchTimestamp = DateTime.now():FormatLocalTime("LTS", "en-us")
|
||||
local historyRecording = ChangeHistoryService:TryBeginRecording("Rojo: Patch " .. patchTimestamp)
|
||||
if not historyRecording then
|
||||
-- There can only be one recording at a time
|
||||
Log.debug("Failed to begin history recording for " .. patchTimestamp .. ". Another recording is in progress.")
|
||||
end
|
||||
|
||||
-- Tracks any portions of the patch that could not be applied to the DOM.
|
||||
local unappliedPatch = PatchSet.newEmpty()
|
||||
|
||||
-- Contains a list of all of the ref properties that we'll need to assign.
|
||||
-- It is imperative that refs are assigned after all instances are created
|
||||
-- to ensure that referents can be mapped to instances correctly.
|
||||
local deferredRefs = {}
|
||||
|
||||
for _, removedIdOrInstance in ipairs(patch.removed) do
|
||||
local removeInstanceSuccess = pcall(function()
|
||||
if Types.RbxId(removedIdOrInstance) then
|
||||
@@ -67,9 +64,6 @@ local function applyPatch(instanceMap, patch)
|
||||
if parentInstance == nil then
|
||||
-- This would be peculiar. If you create an instance with no
|
||||
-- parent, were you supposed to create it at all?
|
||||
if historyRecording then
|
||||
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
|
||||
end
|
||||
invariant(
|
||||
"Cannot add an instance from a patch that has no parent.\nInstance {} with parent {}.\nState: {:#?}",
|
||||
id,
|
||||
@@ -78,7 +72,7 @@ local function applyPatch(instanceMap, patch)
|
||||
)
|
||||
end
|
||||
|
||||
local failedToReify = reify(instanceMap, patch.added, id, parentInstance)
|
||||
local failedToReify = reifyInstance(deferredRefs, instanceMap, patch.added, id, parentInstance)
|
||||
|
||||
if not PatchSet.isEmpty(failedToReify) then
|
||||
Log.debug("Failed to reify as part of applying a patch: {:#?}", failedToReify)
|
||||
@@ -143,7 +137,7 @@ local function applyPatch(instanceMap, patch)
|
||||
[update.id] = mockVirtualInstance,
|
||||
}
|
||||
|
||||
local failedToReify = reify(instanceMap, mockAdded, update.id, instance.Parent)
|
||||
local failedToReify = reifyInstance(deferredRefs, instanceMap, mockAdded, update.id, instance.Parent)
|
||||
|
||||
local newInstance = instanceMap.fromIds[update.id]
|
||||
|
||||
@@ -206,6 +200,18 @@ local function applyPatch(instanceMap, patch)
|
||||
|
||||
if update.changedProperties ~= nil then
|
||||
for propertyName, propertyValue in pairs(update.changedProperties) do
|
||||
-- Because refs may refer to instances that we haven't constructed yet,
|
||||
-- we defer applying any ref properties until all instances are created.
|
||||
if next(propertyValue) == "Ref" then
|
||||
table.insert(deferredRefs, {
|
||||
id = update.id,
|
||||
instance = instance,
|
||||
propertyName = propertyName,
|
||||
virtualValue = propertyValue,
|
||||
})
|
||||
continue
|
||||
end
|
||||
|
||||
local decodeSuccess, decodedValue = decodeValue(propertyValue, instanceMap)
|
||||
if not decodeSuccess then
|
||||
unappliedUpdate.changedProperties[propertyName] = propertyValue
|
||||
@@ -226,9 +232,7 @@ local function applyPatch(instanceMap, patch)
|
||||
end
|
||||
end
|
||||
|
||||
if historyRecording then
|
||||
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
|
||||
end
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
|
||||
return unappliedPatch
|
||||
end
|
||||
|
||||
@@ -25,6 +25,14 @@ local function trueEquals(a, b): boolean
|
||||
return true
|
||||
end
|
||||
|
||||
-- Treat nil and { Ref = "000...0" } as equal
|
||||
if
|
||||
(a == nil and type(b) == "table" and b.Ref == "00000000000000000000000000000000")
|
||||
or (b == nil and type(a) == "table" and a.Ref == "00000000000000000000000000000000")
|
||||
then
|
||||
return true
|
||||
end
|
||||
|
||||
local typeA, typeB = typeof(a), typeof(b)
|
||||
|
||||
-- For tables, try recursive deep equality
|
||||
@@ -151,7 +159,24 @@ local function diff(instanceMap, virtualInstances, rootId)
|
||||
|
||||
if getProperySuccess then
|
||||
local existingValue = existingValueOrErr
|
||||
local decodeSuccess, decodedValue = decodeValue(virtualValue, instanceMap)
|
||||
local decodeSuccess, decodedValue
|
||||
|
||||
-- If `virtualValue` is a ref then instead of decoding it to an instance,
|
||||
-- we change `existingValue` to be a ref. This is because `virtualValue`
|
||||
-- may point to an Instance which doesn't exist yet and therefore
|
||||
-- decoding it may throw an error.
|
||||
if next(virtualValue) == "Ref" then
|
||||
decodeSuccess, decodedValue = true, virtualValue
|
||||
|
||||
if existingValue and typeof(existingValue) == "Instance" then
|
||||
local existingValueRef = instanceMap.fromInstances[existingValue]
|
||||
if existingValueRef then
|
||||
existingValue = { Ref = existingValueRef }
|
||||
end
|
||||
end
|
||||
else
|
||||
decodeSuccess, decodedValue = decodeValue(virtualValue, instanceMap)
|
||||
end
|
||||
|
||||
if decodeSuccess then
|
||||
if not trueEquals(existingValue, decodedValue) then
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local Timer = require(Plugin.Timer)
|
||||
|
||||
@@ -22,78 +19,17 @@ function Reconciler.new(instanceMap)
|
||||
local self = {
|
||||
-- Tracks all of the instances known by the reconciler by ID.
|
||||
__instanceMap = instanceMap,
|
||||
__precommitCallbacks = {},
|
||||
__postcommitCallbacks = {},
|
||||
}
|
||||
|
||||
return setmetatable(self, Reconciler)
|
||||
end
|
||||
|
||||
function Reconciler:hookPrecommit(callback: (patch: any, instanceMap: any) -> ()): () -> ()
|
||||
table.insert(self.__precommitCallbacks, callback)
|
||||
Log.trace("Added precommit callback: {}", callback)
|
||||
|
||||
return function()
|
||||
-- Remove the callback from the list
|
||||
for i, cb in self.__precommitCallbacks do
|
||||
if cb == callback then
|
||||
table.remove(self.__precommitCallbacks, i)
|
||||
Log.trace("Removed precommit callback: {}", callback)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Reconciler:hookPostcommit(callback: (patch: any, instanceMap: any, unappliedPatch: any) -> ()): () -> ()
|
||||
table.insert(self.__postcommitCallbacks, callback)
|
||||
Log.trace("Added postcommit callback: {}", callback)
|
||||
|
||||
return function()
|
||||
-- Remove the callback from the list
|
||||
for i, cb in self.__postcommitCallbacks do
|
||||
if cb == callback then
|
||||
table.remove(self.__postcommitCallbacks, i)
|
||||
Log.trace("Removed postcommit callback: {}", callback)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Reconciler:applyPatch(patch)
|
||||
Timer.start("Reconciler:applyPatch")
|
||||
|
||||
Timer.start("precommitCallbacks")
|
||||
-- Precommit callbacks must be serial in order to obey the contract that
|
||||
-- they execute before commit
|
||||
for _, callback in self.__precommitCallbacks do
|
||||
local success, err = pcall(callback, patch, self.__instanceMap)
|
||||
if not success then
|
||||
Log.warn("Precommit hook errored: {}", err)
|
||||
end
|
||||
end
|
||||
Timer.stop()
|
||||
|
||||
Timer.start("apply")
|
||||
local unappliedPatch = applyPatch(self.__instanceMap, patch)
|
||||
Timer.stop()
|
||||
|
||||
Timer.start("postcommitCallbacks")
|
||||
-- Postcommit callbacks can be called with spawn since regardless of firing order, they are
|
||||
-- guaranteed to be called after the commit
|
||||
for _, callback in self.__postcommitCallbacks do
|
||||
task.spawn(function()
|
||||
local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch)
|
||||
if not success then
|
||||
Log.warn("Postcommit hook errored: {}", err)
|
||||
end
|
||||
end)
|
||||
end
|
||||
Timer.stop()
|
||||
|
||||
Timer.stop()
|
||||
|
||||
return unappliedPatch
|
||||
end
|
||||
|
||||
|
||||
@@ -7,26 +7,6 @@ local PatchSet = require(script.Parent.Parent.PatchSet)
|
||||
local setProperty = require(script.Parent.setProperty)
|
||||
local decodeValue = require(script.Parent.decodeValue)
|
||||
|
||||
local reifyInner, applyDeferredRefs
|
||||
|
||||
local function reify(instanceMap, virtualInstances, rootId, parentInstance)
|
||||
-- Create an empty patch that will be populated with any parts of this reify
|
||||
-- that could not happen, like instances that couldn't be created and
|
||||
-- properties that could not be assigned.
|
||||
local unappliedPatch = PatchSet.newEmpty()
|
||||
|
||||
-- Contains a list of all of the ref properties that we'll need to assign
|
||||
-- after all instances are created. We apply refs in a second pass, after
|
||||
-- we create as many instances as we can, so that we ensure that referents
|
||||
-- can be mapped to instances correctly.
|
||||
local deferredRefs = {}
|
||||
|
||||
reifyInner(instanceMap, virtualInstances, rootId, parentInstance, unappliedPatch, deferredRefs)
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
|
||||
return unappliedPatch
|
||||
end
|
||||
|
||||
--[[
|
||||
Add the given ID and all of its descendants in virtualInstances to the given
|
||||
PatchSet, marked for addition.
|
||||
@@ -40,10 +20,21 @@ local function addAllToPatch(patchSet, virtualInstances, id)
|
||||
end
|
||||
end
|
||||
|
||||
function reifyInstance(deferredRefs, instanceMap, virtualInstances, rootId, parentInstance)
|
||||
-- Create an empty patch that will be populated with any parts of this reify
|
||||
-- that could not happen, like instances that couldn't be created and
|
||||
-- properties that could not be assigned.
|
||||
local unappliedPatch = PatchSet.newEmpty()
|
||||
|
||||
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, rootId, parentInstance)
|
||||
|
||||
return unappliedPatch
|
||||
end
|
||||
|
||||
--[[
|
||||
Inner function that defines the core routine.
|
||||
]]
|
||||
function reifyInner(instanceMap, virtualInstances, id, parentInstance, unappliedPatch, deferredRefs)
|
||||
function reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, id, parentInstance)
|
||||
local virtualInstance = virtualInstances[id]
|
||||
|
||||
if virtualInstance == nil then
|
||||
@@ -102,7 +93,7 @@ function reifyInner(instanceMap, virtualInstances, id, parentInstance, unapplied
|
||||
end
|
||||
|
||||
for _, childId in ipairs(virtualInstance.Children) do
|
||||
reifyInner(instanceMap, virtualInstances, childId, instance, unappliedPatch, deferredRefs)
|
||||
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, childId, instance)
|
||||
end
|
||||
|
||||
instance.Parent = parentInstance
|
||||
@@ -143,6 +134,7 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
end
|
||||
|
||||
local targetInstance = instanceMap.fromIds[refId]
|
||||
|
||||
if targetInstance == nil then
|
||||
markFailed(entry.id, entry.propertyName, entry.virtualValue)
|
||||
continue
|
||||
@@ -155,4 +147,7 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
end
|
||||
end
|
||||
|
||||
return reify
|
||||
return {
|
||||
reifyInstance = reifyInstance,
|
||||
applyDeferredRefs = applyDeferredRefs,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
return function()
|
||||
local reify = require(script.Parent.reify)
|
||||
local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferredRefs
|
||||
|
||||
local PatchSet = require(script.Parent.Parent.PatchSet)
|
||||
local InstanceMap = require(script.Parent.Parent.InstanceMap)
|
||||
@@ -20,7 +21,11 @@ return function()
|
||||
|
||||
it("should throw when given a bogus ID", function()
|
||||
expect(function()
|
||||
reify(InstanceMap.new(), {}, "Hi, mom!", game)
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, {}, "Hi, mom!", game)
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
@@ -34,8 +39,11 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT", nil)
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT", nil)
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
|
||||
assert(instanceMap:size() == 0, "expected instanceMap to be empty")
|
||||
|
||||
@@ -60,8 +68,11 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
@@ -90,8 +101,11 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
@@ -122,8 +136,11 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
|
||||
expect(size(unappliedPatch.added)).to.equal(1)
|
||||
expect(unappliedPatch.added["CHILD"]).to.equal(virtualInstances["CHILD"])
|
||||
@@ -153,8 +170,11 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
|
||||
local instance = instanceMap.fromIds["ROOT"]
|
||||
expect(instance.ClassName).to.equal("StringValue")
|
||||
@@ -196,8 +216,11 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
@@ -223,13 +246,16 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
|
||||
local existing = Instance.new("Folder")
|
||||
existing.Name = "Existing"
|
||||
instanceMap:insert("EXISTING", existing)
|
||||
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
@@ -268,8 +294,11 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
@@ -307,8 +336,11 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
@@ -332,8 +364,11 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
|
||||
assert(not PatchSet.hasRemoves(unappliedPatch), "expected no removes")
|
||||
assert(not PatchSet.hasAdditions(unappliedPatch), "expected no additions")
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
local StudioService = game:GetService("StudioService")
|
||||
local RunService = game:GetService("RunService")
|
||||
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
||||
local SerializationService = game:GetService("SerializationService")
|
||||
local Selection = game:GetService("Selection")
|
||||
|
||||
local Packages = script.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
local Fmt = require(Packages.Fmt)
|
||||
local t = require(Packages.t)
|
||||
local Promise = require(Packages.Promise)
|
||||
local Timer = require(script.Parent.Timer)
|
||||
|
||||
local ChangeBatcher = require(script.Parent.ChangeBatcher)
|
||||
local encodePatchUpdate = require(script.Parent.ChangeBatcher.encodePatchUpdate)
|
||||
@@ -44,6 +48,12 @@ local function debugPatch(object)
|
||||
end)
|
||||
end
|
||||
|
||||
local function attemptReparent(instance, parent)
|
||||
return pcall(function()
|
||||
instance.Parent = parent
|
||||
end)
|
||||
end
|
||||
|
||||
local ServeSession = {}
|
||||
ServeSession.__index = ServeSession
|
||||
|
||||
@@ -95,6 +105,9 @@ function ServeSession.new(options)
|
||||
__changeBatcher = changeBatcher,
|
||||
__statusChangedCallback = nil,
|
||||
__connections = connections,
|
||||
__precommitCallbacks = {},
|
||||
__postcommitCallbacks = {},
|
||||
__updateLoadingText = function() end,
|
||||
}
|
||||
|
||||
setmetatable(self, ServeSession)
|
||||
@@ -125,24 +138,68 @@ function ServeSession:setConfirmCallback(callback)
|
||||
self.__userConfirmCallback = callback
|
||||
end
|
||||
|
||||
function ServeSession:hookPrecommit(callback)
|
||||
return self.__reconciler:hookPrecommit(callback)
|
||||
function ServeSession:setUpdateLoadingTextCallback(callback)
|
||||
self.__updateLoadingText = callback
|
||||
end
|
||||
|
||||
function ServeSession:setLoadingText(text: string)
|
||||
self.__updateLoadingText(text)
|
||||
end
|
||||
|
||||
--[=[
|
||||
Hooks a function to run before patch application.
|
||||
The provided function is called with the incoming patch and an InstanceMap
|
||||
as parameters.
|
||||
]=]
|
||||
function ServeSession:hookPrecommit(callback)
|
||||
table.insert(self.__precommitCallbacks, callback)
|
||||
Log.trace("Added precommit callback: {}", callback)
|
||||
|
||||
return function()
|
||||
-- Remove the callback from the list
|
||||
for i, cb in self.__precommitCallbacks do
|
||||
if cb == callback then
|
||||
table.remove(self.__precommitCallbacks, i)
|
||||
Log.trace("Removed precommit callback: {}", callback)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[=[
|
||||
Hooks a function to run after patch application.
|
||||
The provided function is called with the applied patch, the current
|
||||
InstanceMap, and a PatchSet containing any unapplied changes.
|
||||
]=]
|
||||
function ServeSession:hookPostcommit(callback)
|
||||
return self.__reconciler:hookPostcommit(callback)
|
||||
table.insert(self.__postcommitCallbacks, callback)
|
||||
Log.trace("Added postcommit callback: {}", callback)
|
||||
|
||||
return function()
|
||||
-- Remove the callback from the list
|
||||
for i, cb in self.__postcommitCallbacks do
|
||||
if cb == callback then
|
||||
table.remove(self.__postcommitCallbacks, i)
|
||||
Log.trace("Removed postcommit callback: {}", callback)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ServeSession:start()
|
||||
self:__setStatus(Status.Connecting)
|
||||
self:setLoadingText("Connecting to server...")
|
||||
|
||||
self.__apiContext
|
||||
:connect()
|
||||
:andThen(function(serverInfo)
|
||||
self:__applyGameAndPlaceId(serverInfo)
|
||||
|
||||
self:setLoadingText("Loading initial data from server...")
|
||||
return self:__initialSync(serverInfo):andThen(function()
|
||||
self:setLoadingText("Starting sync loop...")
|
||||
self:__setStatus(Status.Connected, serverInfo.projectName)
|
||||
self:__applyGameAndPlaceId(serverInfo)
|
||||
|
||||
return self:__mainSyncLoop()
|
||||
end)
|
||||
@@ -207,6 +264,194 @@ function ServeSession:__onActiveScriptChanged(activeScript)
|
||||
self.__apiContext:open(scriptId)
|
||||
end
|
||||
|
||||
function ServeSession:__replaceInstances(idList)
|
||||
if #idList == 0 then
|
||||
return true, PatchSet.newEmpty()
|
||||
end
|
||||
-- It would be annoying if selection went away, so we try to preserve it.
|
||||
local selection = Selection:Get()
|
||||
local selectionMap = {}
|
||||
for i, instance in selection do
|
||||
selectionMap[instance] = i
|
||||
end
|
||||
|
||||
-- TODO: Should we do this in multiple requests so we can more granularly mark failures?
|
||||
local modelSuccess, replacements = self.__apiContext
|
||||
:serialize(idList)
|
||||
:andThen(function(response)
|
||||
Log.debug("Deserializing results from serialize endpoint")
|
||||
local objects = SerializationService:DeserializeInstancesAsync(response.modelContents)
|
||||
if not objects[1] then
|
||||
return Promise.reject("Serialize endpoint did not deserialize into any Instances")
|
||||
end
|
||||
if #objects[1]:GetChildren() ~= #idList then
|
||||
return Promise.reject("Serialize endpoint did not return the correct number of Instances")
|
||||
end
|
||||
|
||||
local instanceMap = {}
|
||||
for _, item in objects[1]:GetChildren() do
|
||||
instanceMap[item.Name] = item.Value
|
||||
end
|
||||
return instanceMap
|
||||
end)
|
||||
:await()
|
||||
|
||||
local refSuccess, refPatch = self.__apiContext
|
||||
:refPatch(idList)
|
||||
:andThen(function(response)
|
||||
return response.patch
|
||||
end)
|
||||
:await()
|
||||
|
||||
if not (modelSuccess and refSuccess) then
|
||||
return false
|
||||
end
|
||||
|
||||
for id, replacement in replacements do
|
||||
local oldInstance = self.__instanceMap.fromIds[id]
|
||||
if not oldInstance then
|
||||
-- TODO: Why would this happen?
|
||||
Log.warn("Instance {} not found in InstanceMap during sync replacement", id)
|
||||
continue
|
||||
end
|
||||
|
||||
self.__instanceMap:insert(id, replacement)
|
||||
Log.trace("Swapping Instance {} out via api/models/ endpoint", id)
|
||||
local oldParent = oldInstance.Parent
|
||||
for _, child in oldInstance:GetChildren() do
|
||||
-- Some children cannot be reparented, such as a TouchTransmitter
|
||||
local reparentSuccess, reparentError = attemptReparent(child, replacement)
|
||||
if not reparentSuccess then
|
||||
Log.warn(
|
||||
"Could not reparent child {} of instance {} during sync replacement: {}",
|
||||
child.Name,
|
||||
oldInstance.Name,
|
||||
reparentError
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
-- ChangeHistoryService doesn't like it if an Instance has been
|
||||
-- Destroyed. So, we have to accept the potential memory hit and
|
||||
-- just set the parent to `nil`.
|
||||
local deleteSuccess, deleteError = attemptReparent(oldInstance, nil)
|
||||
local replaceSuccess, replaceError = attemptReparent(replacement, oldParent)
|
||||
|
||||
if not (deleteSuccess and replaceSuccess) then
|
||||
Log.warn(
|
||||
"Could not swap instances {} and {} during sync replacement: {}",
|
||||
oldInstance.Name,
|
||||
replacement.Name,
|
||||
(deleteError or "") .. "\n" .. (replaceError or "")
|
||||
)
|
||||
|
||||
-- We need to revert the failed swap to avoid losing the old instance and children.
|
||||
for _, child in replacement:GetChildren() do
|
||||
attemptReparent(child, oldInstance)
|
||||
end
|
||||
attemptReparent(oldInstance, oldParent)
|
||||
|
||||
-- Our replacement should never have existed in the first place, so we can just destroy it.
|
||||
replacement:Destroy()
|
||||
continue
|
||||
end
|
||||
|
||||
if selectionMap[oldInstance] then
|
||||
-- This is a bit funky, but it saves the order of Selection
|
||||
-- which might matter for some use cases.
|
||||
selection[selectionMap[oldInstance]] = replacement
|
||||
end
|
||||
end
|
||||
|
||||
local patchApplySuccess, unappliedPatch = pcall(self.__reconciler.applyPatch, self.__reconciler, refPatch)
|
||||
if patchApplySuccess then
|
||||
Selection:Set(selection)
|
||||
return true, unappliedPatch
|
||||
else
|
||||
error(unappliedPatch)
|
||||
end
|
||||
end
|
||||
|
||||
function ServeSession:__applyPatch(patch)
|
||||
local patchTimestamp = DateTime.now():FormatLocalTime("LTS", "en-us")
|
||||
local historyRecording = ChangeHistoryService:TryBeginRecording("Rojo: Patch " .. patchTimestamp)
|
||||
if not historyRecording then
|
||||
-- There can only be one recording at a time
|
||||
Log.debug("Failed to begin history recording for " .. patchTimestamp .. ". Another recording is in progress.")
|
||||
end
|
||||
|
||||
Timer.start("precommitCallbacks")
|
||||
-- Precommit callbacks must be serial in order to obey the contract that
|
||||
-- they execute before commit
|
||||
for _, callback in self.__precommitCallbacks do
|
||||
local success, err = pcall(callback, patch, self.__instanceMap)
|
||||
if not success then
|
||||
Log.warn("Precommit hook errored: {}", err)
|
||||
end
|
||||
end
|
||||
Timer.stop()
|
||||
|
||||
local patchApplySuccess, unappliedPatch = pcall(self.__reconciler.applyPatch, self.__reconciler, patch)
|
||||
if not patchApplySuccess then
|
||||
if historyRecording then
|
||||
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
|
||||
end
|
||||
-- This might make a weird stack trace but the only way applyPatch can
|
||||
-- fail is if a bug occurs so it's probably fine.
|
||||
error(unappliedPatch)
|
||||
end
|
||||
|
||||
if Settings:get("enableSyncFallback") and not PatchSet.isEmpty(unappliedPatch) then
|
||||
-- Some changes did not apply, let's try replacing them instead
|
||||
local addedIdList = PatchSet.addedIdList(unappliedPatch)
|
||||
local updatedIdList = PatchSet.updatedIdList(unappliedPatch)
|
||||
|
||||
Log.debug("ServeSession:__replaceInstances(unappliedPatch.added)")
|
||||
Timer.start("ServeSession:__replaceInstances(unappliedPatch.added)")
|
||||
local addSuccess, unappliedAddedRefs = self:__replaceInstances(addedIdList)
|
||||
Timer.stop()
|
||||
|
||||
Log.debug("ServeSession:__replaceInstances(unappliedPatch.updated)")
|
||||
Timer.start("ServeSession:__replaceInstances(unappliedPatch.updated)")
|
||||
local updateSuccess, unappliedUpdateRefs = self:__replaceInstances(updatedIdList)
|
||||
Timer.stop()
|
||||
|
||||
-- Update the unapplied patch to reflect which Instances were replaced successfully
|
||||
if addSuccess then
|
||||
table.clear(unappliedPatch.added)
|
||||
PatchSet.assign(unappliedPatch, unappliedAddedRefs)
|
||||
end
|
||||
if updateSuccess then
|
||||
table.clear(unappliedPatch.updated)
|
||||
PatchSet.assign(unappliedPatch, unappliedUpdateRefs)
|
||||
end
|
||||
end
|
||||
|
||||
if not PatchSet.isEmpty(unappliedPatch) then
|
||||
Log.debug(
|
||||
"Could not apply all changes requested by the Rojo server:\n{}",
|
||||
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)
|
||||
)
|
||||
end
|
||||
|
||||
Timer.start("postcommitCallbacks")
|
||||
-- Postcommit callbacks can be called with spawn since regardless of firing order, they are
|
||||
-- guaranteed to be called after the commit
|
||||
for _, callback in self.__postcommitCallbacks do
|
||||
task.spawn(function()
|
||||
local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch)
|
||||
if not success then
|
||||
Log.warn("Postcommit hook errored: {}", err)
|
||||
end
|
||||
end)
|
||||
end
|
||||
Timer.stop()
|
||||
|
||||
if historyRecording then
|
||||
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
|
||||
end
|
||||
end
|
||||
|
||||
function ServeSession:__initialSync(serverInfo)
|
||||
return self.__apiContext:read({ serverInfo.rootInstanceId }):andThen(function(readResponseBody)
|
||||
-- Tell the API Context that we're up-to-date with the version of
|
||||
@@ -216,11 +461,13 @@ function ServeSession:__initialSync(serverInfo)
|
||||
-- For any instances that line up with the Rojo server's view, start
|
||||
-- tracking them in the reconciler.
|
||||
Log.trace("Matching existing Roblox instances to Rojo IDs")
|
||||
self:setLoadingText("Hydrating instance map...")
|
||||
self.__reconciler:hydrate(readResponseBody.instances, serverInfo.rootInstanceId, game)
|
||||
|
||||
-- Calculate the initial patch to apply to the DataModel to catch us
|
||||
-- up to what Rojo thinks the place should look like.
|
||||
Log.trace("Computing changes that plugin needs to make to catch up to server...")
|
||||
self:setLoadingText("Finding differences between server and Studio...")
|
||||
local success, catchUpPatch =
|
||||
self.__reconciler:diff(readResponseBody.instances, serverInfo.rootInstanceId, game)
|
||||
|
||||
@@ -281,15 +528,7 @@ function ServeSession:__initialSync(serverInfo)
|
||||
|
||||
return self.__apiContext:write(inversePatch)
|
||||
elseif userDecision == "Accept" then
|
||||
local unappliedPatch = self.__reconciler:applyPatch(catchUpPatch)
|
||||
|
||||
if not PatchSet.isEmpty(unappliedPatch) then
|
||||
Log.debug(
|
||||
"Could not apply all changes requested by the Rojo server:\n{}",
|
||||
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)
|
||||
)
|
||||
end
|
||||
|
||||
self:__applyPatch(catchUpPatch)
|
||||
return Promise.resolve()
|
||||
else
|
||||
return Promise.reject("Invalid user decision: " .. userDecision)
|
||||
@@ -312,14 +551,7 @@ function ServeSession:__mainSyncLoop()
|
||||
Log.trace("Serve session {} retrieved {} messages", tostring(self), #messages)
|
||||
|
||||
for _, message in messages do
|
||||
local unappliedPatch = self.__reconciler:applyPatch(message)
|
||||
|
||||
if not PatchSet.isEmpty(unappliedPatch) then
|
||||
Log.debug(
|
||||
"Could not apply all changes requested by the Rojo server:\n{}",
|
||||
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)
|
||||
)
|
||||
end
|
||||
self:__applyPatch(message)
|
||||
end
|
||||
end)
|
||||
:await()
|
||||
|
||||
@@ -12,12 +12,15 @@ local Roact = require(Packages.Roact)
|
||||
local defaultSettings = {
|
||||
openScriptsExternally = false,
|
||||
twoWaySync = false,
|
||||
autoReconnect = false,
|
||||
showNotifications = true,
|
||||
syncReminder = true,
|
||||
enableSyncFallback = true,
|
||||
syncReminderMode = "Notify" :: "None" | "Notify" | "Fullscreen",
|
||||
syncReminderPolling = true,
|
||||
checkForUpdates = true,
|
||||
checkForPrereleases = false,
|
||||
autoConnectPlaytestServer = false,
|
||||
confirmationBehavior = "Initial",
|
||||
confirmationBehavior = "Initial" :: "Never" | "Initial" | "Large Changes" | "Unlisted PlaceId",
|
||||
largeChangesConfirmationThreshold = 5,
|
||||
playSounds = true,
|
||||
typecheckingEnabled = false,
|
||||
@@ -108,4 +111,14 @@ function Settings:getBinding(name)
|
||||
return bind
|
||||
end
|
||||
|
||||
function Settings:getBindings(...: string)
|
||||
local bindings = {}
|
||||
for i = 1, select("#", ...) do
|
||||
local source = select(i, ...)
|
||||
bindings[source] = self:getBinding(source)
|
||||
end
|
||||
|
||||
return Roact.joinBindings(bindings)
|
||||
end
|
||||
|
||||
return Settings
|
||||
|
||||
@@ -55,6 +55,16 @@ local ApiSubscribeResponse = t.interface({
|
||||
messages = t.array(ApiSubscribeMessage),
|
||||
})
|
||||
|
||||
local ApiSerializeResponse = t.interface({
|
||||
sessionId = t.string,
|
||||
modelContents = t.buffer,
|
||||
})
|
||||
|
||||
local ApiRefPatchResponse = t.interface({
|
||||
sessionId = t.string,
|
||||
patch = ApiSubscribeMessage,
|
||||
})
|
||||
|
||||
local ApiError = t.interface({
|
||||
kind = t.union(t.literal("NotFound"), t.literal("BadRequest"), t.literal("InternalError")),
|
||||
details = t.string,
|
||||
@@ -82,6 +92,8 @@ return strict("Types", {
|
||||
ApiInstanceUpdate = ApiInstanceUpdate,
|
||||
ApiInstanceMetadata = ApiInstanceMetadata,
|
||||
ApiSubscribeMessage = ApiSubscribeMessage,
|
||||
ApiSerializeResponse = ApiSerializeResponse,
|
||||
ApiRefPatchResponse = ApiRefPatchResponse,
|
||||
ApiValue = ApiValue,
|
||||
RbxId = RbxId,
|
||||
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
local Packages = script.Parent.Parent.Packages
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Http = require(Packages.Http)
|
||||
local Promise = require(Packages.Promise)
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local Config = require(Plugin.Config)
|
||||
local Settings = require(Plugin.Settings)
|
||||
local timeUtil = require(Plugin.timeUtil)
|
||||
|
||||
type LatestReleaseInfo = {
|
||||
version: { number },
|
||||
prerelease: boolean,
|
||||
publishedUnixTimestamp: number,
|
||||
}
|
||||
|
||||
local function compare(a, b)
|
||||
if a > b then
|
||||
@@ -88,14 +102,26 @@ function Version.display(version)
|
||||
return output
|
||||
end
|
||||
|
||||
--[[
|
||||
The GitHub API rate limit for unauthenticated requests is rather low,
|
||||
and we don't release often enough to warrant checking it more than once a day.
|
||||
--]]
|
||||
Version._cachedLatestCompatible = nil :: {
|
||||
value: LatestReleaseInfo?,
|
||||
timestamp: number,
|
||||
}?
|
||||
|
||||
function Version.retrieveLatestCompatible(options: {
|
||||
version: { number },
|
||||
includePrereleases: boolean?,
|
||||
}): {
|
||||
version: { number },
|
||||
prerelease: boolean,
|
||||
publishedUnixTimestamp: number,
|
||||
}?
|
||||
}): LatestReleaseInfo?
|
||||
if Version._cachedLatestCompatible and os.clock() - Version._cachedLatestCompatible.timestamp < 60 * 60 * 24 then
|
||||
Log.debug("Using cached latest compatible version")
|
||||
return Version._cachedLatestCompatible.value
|
||||
end
|
||||
|
||||
Log.debug("Retrieving latest compatible version from GitHub")
|
||||
|
||||
local success, releases = Http.get("https://api.github.com/repos/rojo-rbx/rojo/releases?per_page=10")
|
||||
:andThen(function(response)
|
||||
if response.code >= 400 then
|
||||
@@ -114,7 +140,7 @@ function Version.retrieveLatestCompatible(options: {
|
||||
end
|
||||
|
||||
-- Iterate through releases, looking for the latest compatible version
|
||||
local latestCompatible = nil
|
||||
local latestCompatible: LatestReleaseInfo? = nil
|
||||
for _, release in releases do
|
||||
-- Skip prereleases if they are not requested
|
||||
if (not options.includePrereleases) and release.prerelease then
|
||||
@@ -142,10 +168,43 @@ function Version.retrieveLatestCompatible(options: {
|
||||
|
||||
-- Don't return anything if the latest found is not newer than the current version
|
||||
if latestCompatible == nil or Version.compare(latestCompatible.version, options.version) <= 0 then
|
||||
-- Cache as nil so we don't try again for a day
|
||||
Version._cachedLatestCompatible = {
|
||||
value = nil,
|
||||
timestamp = os.clock(),
|
||||
}
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Cache the latest compatible version
|
||||
Version._cachedLatestCompatible = {
|
||||
value = latestCompatible,
|
||||
timestamp = os.clock(),
|
||||
}
|
||||
|
||||
return latestCompatible
|
||||
end
|
||||
|
||||
function Version.getUpdateMessage(): string?
|
||||
if not Settings:get("checkForUpdates") then
|
||||
return
|
||||
end
|
||||
|
||||
local isLocalInstall = string.find(debug.traceback(), "\n[^\n]-user_.-$") ~= nil
|
||||
local latestCompatibleVersion = Version.retrieveLatestCompatible({
|
||||
version = Config.version,
|
||||
includePrereleases = isLocalInstall and Settings:get("checkForPrereleases"),
|
||||
})
|
||||
if not latestCompatibleVersion then
|
||||
return
|
||||
end
|
||||
|
||||
return string.format(
|
||||
"A newer compatible version of Rojo, %s, was published %s! Go to the Rojo releases page to learn more.",
|
||||
Version.display(latestCompatibleVersion.version),
|
||||
timeUtil.elapsedToText(DateTime.now().UnixTimestamp - latestCompatibleVersion.publishedUnixTimestamp)
|
||||
)
|
||||
end
|
||||
|
||||
return Version
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
rojo build test-place.project.json -o TestPlace.rbxlx
|
||||
run-in-roblox --script run-tests.server.lua --place TestPlace.rbxlx
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
"ReplicatedStorage": {
|
||||
"Rojo": {
|
||||
"$path": "default.project.json"
|
||||
"$path": "../plugin.project.json"
|
||||
},
|
||||
|
||||
"Packages": {
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# Continously build the rojo plugin into the local plugin directory on Windows
|
||||
rojo build plugin/default.project.json -o $LOCALAPPDATA/Roblox/Plugins/Rojo.rbxm --watch
|
||||
@@ -6,6 +6,7 @@ expression: contents
|
||||
<Item class="Model" referent="0">
|
||||
<Properties>
|
||||
<string name="Name">init_meta_class_name</string>
|
||||
<bool name="NeedsPivotMigration">false</bool>
|
||||
</Properties>
|
||||
</Item>
|
||||
</roblox>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: tests/tests/build.rs
|
||||
expression: contents
|
||||
|
||||
---
|
||||
<roblox version="4">
|
||||
<Item class="Folder" referent="0">
|
||||
@@ -25,6 +24,7 @@ expression: contents
|
||||
<R21>0</R21>
|
||||
<R22>1</R22>
|
||||
</CoordinateFrame>
|
||||
<bool name="NeedsPivotMigration">false</bool>
|
||||
<Ref name="PrimaryPart">null</Ref>
|
||||
<BinaryString name="Tags"></BinaryString>
|
||||
</Properties>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: tests/tests/build.rs
|
||||
expression: contents
|
||||
|
||||
---
|
||||
<roblox version="4">
|
||||
<Item class="DataModel" referent="0">
|
||||
@@ -22,6 +21,7 @@ expression: contents
|
||||
<Item class="Workspace" referent="2">
|
||||
<Properties>
|
||||
<string name="Name">Workspace</string>
|
||||
<bool name="NeedsPivotMigration">false</bool>
|
||||
</Properties>
|
||||
<Item class="BoolValue" referent="3">
|
||||
<Properties>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: redactions.redacted_yaml(info)
|
||||
|
||||
---
|
||||
expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
@@ -11,4 +10,4 @@ protocolVersion: 4
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
unexpectedPlaceIds: ~
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: redactions.redacted_yaml(info)
|
||||
|
||||
---
|
||||
expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
@@ -11,4 +10,4 @@ protocolVersion: 4
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
unexpectedPlaceIds: ~
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: redactions.redacted_yaml(info)
|
||||
|
||||
---
|
||||
expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
@@ -11,4 +10,4 @@ protocolVersion: 4
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
unexpectedPlaceIds: ~
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||
|
||||
---
|
||||
instances:
|
||||
id-2:
|
||||
@@ -22,7 +21,8 @@ instances:
|
||||
ignoreUnknownInstances: false
|
||||
Name: test
|
||||
Parent: id-2
|
||||
Properties: {}
|
||||
Properties:
|
||||
NeedsPivotMigration:
|
||||
Bool: false
|
||||
messageCursor: 1
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: redactions.redacted_yaml(info)
|
||||
|
||||
---
|
||||
expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
@@ -11,4 +10,4 @@ protocolVersion: 4
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
unexpectedPlaceIds: ~
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
@@ -14,8 +13,9 @@ messages:
|
||||
ignoreUnknownInstances: false
|
||||
Name: test
|
||||
Parent: id-2
|
||||
Properties: {}
|
||||
Properties:
|
||||
NeedsPivotMigration:
|
||||
Bool: false
|
||||
removed: []
|
||||
updated: []
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user