mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-20 20:55:50 +00:00
Compare commits
48 Commits
aarch-wind
...
plugin-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47db569c52 | ||
|
|
1d3f8c8e9d | ||
|
|
a894313a4b | ||
|
|
7f73ae80dc | ||
|
|
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))))
|
||||||
2
.github/workflows/changelog.yml
vendored
2
.github/workflows/changelog.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
name: Check Actions
|
name: Check Actions
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Changelog check
|
- name: Changelog check
|
||||||
uses: Zomzog/changelog-checker@v1.3.0
|
uses: Zomzog/changelog-checker@v1.3.0
|
||||||
|
|||||||
95
.github/workflows/ci.yml
vendored
95
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-22.04, windows-latest, macos-latest, windows-11-arm, ubuntu-22.04-arm]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -26,13 +26,14 @@ jobs:
|
|||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
- name: Rust cache
|
- name: Restore Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: actions/cache/restore@v4
|
||||||
|
|
||||||
- name: Setup Aftman
|
|
||||||
uses: ok-nick/setup-aftman@v0.3.0
|
|
||||||
with:
|
with:
|
||||||
version: 'v0.2.7'
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --locked --verbose
|
run: cargo build --locked --verbose
|
||||||
@@ -40,6 +41,15 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --locked --verbose
|
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:
|
msrv:
|
||||||
name: Check MSRV
|
name: Check MSRV
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -50,19 +60,50 @@ jobs:
|
|||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.70.0
|
uses: dtolnay/rust-toolchain@1.79.0
|
||||||
|
|
||||||
- name: Rust cache
|
- name: Restore Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: actions/cache/restore@v4
|
||||||
|
|
||||||
- name: Setup Aftman
|
|
||||||
uses: ok-nick/setup-aftman@v0.3.0
|
|
||||||
with:
|
with:
|
||||||
version: 'v0.2.7'
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --locked --verbose
|
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') }}
|
||||||
|
|
||||||
|
test-plugin:
|
||||||
|
name: Test Plugin
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
|
- name: Setup Rokit
|
||||||
|
uses: CompeyDev/setup-rokit@v0.1.2
|
||||||
|
with:
|
||||||
|
version: 'v1.1.0'
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: lune run test-plugin
|
||||||
|
env:
|
||||||
|
RBX_API_KEY: ${{ secrets.PLUGIN_TEST_API_KEY }}
|
||||||
|
RBX_UNIVERSE_ID: ${{ vars.PLUGIN_TEST_UNIVERSE_ID }}
|
||||||
|
RBX_PLACE_ID: ${{ vars.PLUGIN_TEST_PLACE_ID }}
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Rustfmt, Clippy, Stylua, & Selene
|
name: Rustfmt, Clippy, Stylua, & Selene
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -77,13 +118,19 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
|
|
||||||
- name: Rust cache
|
- name: Restore Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: actions/cache/restore@v4
|
||||||
|
|
||||||
- name: Setup Aftman
|
|
||||||
uses: ok-nick/setup-aftman@v0.3.0
|
|
||||||
with:
|
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
|
- name: Stylua
|
||||||
run: stylua --check plugin/src
|
run: stylua --check plugin/src
|
||||||
@@ -97,3 +144,11 @@ jobs:
|
|||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo 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') }}
|
||||||
|
|||||||
51
.github/workflows/release.yml
vendored
51
.github/workflows/release.yml
vendored
@@ -25,15 +25,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Setup Aftman
|
- name: Setup Rokit
|
||||||
uses: ok-nick/setup-aftman@v0.1.0
|
uses: CompeyDev/setup-rokit@v0.1.2
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
version: 'v1.1.0'
|
||||||
trust-check: false
|
|
||||||
version: 'v0.2.6'
|
|
||||||
|
|
||||||
- name: Build Plugin
|
- name: Build Plugin
|
||||||
run: rojo build plugin --output Rojo.rbxm
|
run: rojo build plugin.project.json --output Rojo.rbxm
|
||||||
|
|
||||||
- name: Upload Plugin to Release
|
- name: Upload Plugin to Release
|
||||||
env:
|
env:
|
||||||
@@ -55,15 +53,25 @@ jobs:
|
|||||||
# https://doc.rust-lang.org/rustc/platform-support.html
|
# https://doc.rust-lang.org/rustc/platform-support.html
|
||||||
include:
|
include:
|
||||||
- host: linux
|
- host: linux
|
||||||
os: ubuntu-20.04
|
os: ubuntu-22.04
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
label: linux-x86_64
|
label: linux-x86_64
|
||||||
|
|
||||||
|
- host: linux
|
||||||
|
os: ubuntu-22.04-arm
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
label: linux-aarch64
|
||||||
|
|
||||||
- host: windows
|
- host: windows
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
label: windows-x86_64
|
label: windows-x86_64
|
||||||
|
|
||||||
|
- host: windows
|
||||||
|
os: windows-11-arm
|
||||||
|
target: aarch64-pc-windows-msvc
|
||||||
|
label: windows-aarch64
|
||||||
|
|
||||||
- host: macos
|
- host: macos
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
@@ -88,19 +96,26 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Setup Aftman
|
- name: Restore Rust Cache
|
||||||
uses: ok-nick/setup-aftman@v0.1.0
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
path: |
|
||||||
trust-check: false
|
~/.cargo/registry
|
||||||
version: 'v0.2.6'
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Build Release
|
- name: Build Release
|
||||||
run: cargo build --release --locked --verbose --target ${{ matrix.target }}
|
run: cargo build --release --locked --verbose --target ${{ matrix.target }}
|
||||||
env:
|
|
||||||
# Build into a known directory so we can find our build artifact more
|
- name: Save Rust Cache
|
||||||
# easily.
|
uses: actions/cache/save@v4
|
||||||
CARGO_TARGET_DIR: output
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Generate Artifact Name
|
- name: Generate Artifact Name
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -117,11 +132,11 @@ jobs:
|
|||||||
mkdir staging
|
mkdir staging
|
||||||
|
|
||||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||||
cp "output/${{ matrix.target }}/release/$BIN.exe" staging/
|
cp "target/${{ matrix.target }}/release/$BIN.exe" staging/
|
||||||
cd staging
|
cd staging
|
||||||
7z a ../$ARTIFACT_NAME *
|
7z a ../$ARTIFACT_NAME *
|
||||||
else
|
else
|
||||||
cp "output/${{ matrix.target }}/release/$BIN" staging/
|
cp "target/${{ matrix.target }}/release/$BIN" staging/
|
||||||
cd staging
|
cd staging
|
||||||
zip ../$ARTIFACT_NAME *
|
zip ../$ARTIFACT_NAME *
|
||||||
fi
|
fi
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -10,8 +10,8 @@
|
|||||||
/*.rbxl
|
/*.rbxl
|
||||||
/*.rbxlx
|
/*.rbxlx
|
||||||
|
|
||||||
# Test places for the Roblox Studio Plugin
|
# Sourcemap for the Rojo plugin (for better intellisense)
|
||||||
/plugin/*.rbxlx
|
/sourcemap.json
|
||||||
|
|
||||||
# Roblox Studio holds 'lock' files on places
|
# Roblox Studio holds 'lock' files on places
|
||||||
*.rbxl.lock
|
*.rbxl.lock
|
||||||
@@ -19,3 +19,7 @@
|
|||||||
|
|
||||||
# Snapshot files from the 'insta' Rust crate
|
# Snapshot files from the 'insta' Rust crate
|
||||||
**/*.snap.new
|
**/*.snap.new
|
||||||
|
|
||||||
|
# Macos file system junk
|
||||||
|
._*
|
||||||
|
.DS_STORE
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -16,3 +16,6 @@
|
|||||||
[submodule "plugin/Packages/Highlighter"]
|
[submodule "plugin/Packages/Highlighter"]
|
||||||
path = plugin/Packages/Highlighter
|
path = plugin/Packages/Highlighter
|
||||||
url = https://github.com/boatbomber/highlighter.git
|
url = https://github.com/boatbomber/highlighter.git
|
||||||
|
[submodule ".lune/opencloud-execute"]
|
||||||
|
path = .lune/opencloud-execute
|
||||||
|
url = https://github.com/Dekkonot/opencloud-luau-execute-lune.git
|
||||||
|
|||||||
5
.lune/.luaurc
Normal file
5
.lune/.luaurc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"aliases": {
|
||||||
|
"lune": "~/.lune/.typedefs/0.10.2/"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
.lune/opencloud-execute
Submodule
1
.lune/opencloud-execute
Submodule
Submodule .lune/opencloud-execute added at 8ae86dd3ad
112
.lune/test-plugin.luau
Normal file
112
.lune/test-plugin.luau
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
local serde = require("@lune/serde")
|
||||||
|
local net = require("@lune/net")
|
||||||
|
local stdio = require("@lune/stdio")
|
||||||
|
local process = require("@lune/process")
|
||||||
|
local fs = require("@lune/fs")
|
||||||
|
|
||||||
|
local luau_execute = require("./opencloud-execute")
|
||||||
|
|
||||||
|
local TEST_SCRIPT = fs.readFile("plugin/run-tests.server.lua")
|
||||||
|
|
||||||
|
local PATH_VERSION_MATCH = "assets/%d+/versions/(.+)"
|
||||||
|
|
||||||
|
local UNIVERSE_ID = process.env["RBX_UNIVERSE_ID"]
|
||||||
|
local PLACE_ID = process.env["RBX_PLACE_ID"]
|
||||||
|
local API_KEY = process.env["RBX_API_KEY"]
|
||||||
|
|
||||||
|
if not UNIVERSE_ID then
|
||||||
|
error("no universe ID specified. try providing one with the env var `RBX_UNIVERSE_ID`")
|
||||||
|
end
|
||||||
|
if not PLACE_ID then
|
||||||
|
error("no place ID specified. try providing one with the env var `RBX_PLACE_ID`")
|
||||||
|
end
|
||||||
|
if not API_KEY then
|
||||||
|
error("no API key specified. try providing one with the env var `RBX_API_KEY`")
|
||||||
|
end
|
||||||
|
|
||||||
|
--stylua: ignore
|
||||||
|
local upload_result = process.exec("cargo", {
|
||||||
|
"run", "--",
|
||||||
|
"upload", "plugin/test-place.project.json",
|
||||||
|
"--api_key", API_KEY,
|
||||||
|
"--universe_id", UNIVERSE_ID,
|
||||||
|
"--asset_id", PLACE_ID
|
||||||
|
}, {
|
||||||
|
stdio = "none"
|
||||||
|
})
|
||||||
|
|
||||||
|
if not upload_result.ok then
|
||||||
|
print("Failed to upload plugin test place")
|
||||||
|
print("Not dumping stdout or stderr to avoid leaking secrets")
|
||||||
|
process.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This is /probably/ not necessary because Rojo generally does not have enough
|
||||||
|
-- activity that there will be multiple CI runs happening at once, but
|
||||||
|
-- it's better safe than sorry.
|
||||||
|
local version_response = net.request({
|
||||||
|
method = "GET",
|
||||||
|
url = `https://apis.roblox.com/assets/v1/assets/{PLACE_ID}/versions`,
|
||||||
|
query = {
|
||||||
|
maxPageSize = 1,
|
||||||
|
},
|
||||||
|
headers = {
|
||||||
|
["User-Agent"] = `Rojo/PluginTesting 1.0.0; {_VERSION}`,
|
||||||
|
["x-api-key"] = API_KEY,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if not version_response.ok then
|
||||||
|
error(
|
||||||
|
`Failed to fetch version of Roblox place to run tests on because: {version_response.statusCode} - {version_response.statusMessage}\n{version_response.body}`
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local place_version_raw = serde.decode("json", version_response.body).assetVersions[1].path
|
||||||
|
assert(typeof(place_version_raw) == "string", "the result from asset version endpoint was not as expected")
|
||||||
|
|
||||||
|
local place_version = string.match(place_version_raw, PATH_VERSION_MATCH)
|
||||||
|
|
||||||
|
local task = luau_execute.create_task_versioned(UNIVERSE_ID, PLACE_ID, place_version, TEST_SCRIPT)
|
||||||
|
print(`Running test script on {UNIVERSE_ID}/{PLACE_ID}@{place_version}`)
|
||||||
|
print(`Task ID: {luau_execute.task_id(task)}`)
|
||||||
|
|
||||||
|
luau_execute.await_finish(task)
|
||||||
|
print("Output from task:\n")
|
||||||
|
local logs = luau_execute.get_structured_logs(task)
|
||||||
|
for _, log in logs do
|
||||||
|
if log.messageType == "OUTPUT" or log.messageType == "MESSAGE_TYPE_UNSPECIFIED" then
|
||||||
|
stdio.write(stdio.color("reset"))
|
||||||
|
elseif log.messageType == "INFO" then
|
||||||
|
stdio.write(stdio.color("cyan"))
|
||||||
|
elseif log.messageType == "WARNING" then
|
||||||
|
stdio.write(stdio.color("yellow"))
|
||||||
|
elseif log.messageType == "ERROR" then
|
||||||
|
stdio.write(stdio.color("red"))
|
||||||
|
end
|
||||||
|
stdio.write(log.message)
|
||||||
|
stdio.write(`{stdio.color("reset")}\n`)
|
||||||
|
end
|
||||||
|
|
||||||
|
local results = luau_execute.get_output(task)[1]
|
||||||
|
if not results then
|
||||||
|
error("plugin tests did not return any results")
|
||||||
|
end
|
||||||
|
|
||||||
|
local status = luau_execute.check_status(task)
|
||||||
|
if status == "COMPLETE" then
|
||||||
|
if results.failureCount == 0 then
|
||||||
|
process.exit(0)
|
||||||
|
else
|
||||||
|
process.exit(1)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
print()
|
||||||
|
print("Task did not finish successfully")
|
||||||
|
local err = luau_execute.get_error(task)
|
||||||
|
if err then
|
||||||
|
print(`Error from task: {err.code}`)
|
||||||
|
print(err.message)
|
||||||
|
end
|
||||||
|
process.exit(1)
|
||||||
|
end
|
||||||
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
|
||||||
|
}
|
||||||
71
CHANGELOG.md
71
CHANGELOG.md
@@ -1,6 +1,48 @@
|
|||||||
# Rojo Changelog
|
# Rojo Changelog
|
||||||
|
|
||||||
## Unreleased Changes
|
## Unreleased
|
||||||
|
|
||||||
|
* Added fallback method for when an Instance can't be synced through normal means ([#1030])
|
||||||
|
This should make it possible to sync `MeshParts` and `Unions`!
|
||||||
|
|
||||||
|
The fallback involves deleting and recreating Instances. This will break
|
||||||
|
properties that reference them that Rojo does not know about, so be weary.
|
||||||
|
|
||||||
|
* Add auto-reconnect and improve UX for sync reminders ([#1096])
|
||||||
|
* Add support for syncing `yml` and `yaml` files (behaves similar to JSON and TOML) ([#1093])
|
||||||
|
* Fixed colors of Table diff ([#1084])
|
||||||
|
* Fixed `sourcemap` command outputting paths with OS-specific path separators ([#1085])
|
||||||
|
* Fixed nil -> nil properties showing up as failing to sync in plugin's patch visualizer ([#1081])
|
||||||
|
* Changed the background of the server's in-browser UI to be gray instead of white ([#1080])
|
||||||
|
* Fixed `Auto Connect Playtest Server` no longer functioning due to Roblox change ([#1066])
|
||||||
|
* Added an update indicator to the version header when a new version of the plugin is available. ([#1069])
|
||||||
|
* Added `--absolute` flag to the sourcemap subcommand, which will emit absolute paths instead of relative paths. ([#1092])
|
||||||
|
* Fixed applying `gameId` and `placeId` before initial sync was accepted ([#1104])
|
||||||
|
|
||||||
|
[#1030]: https://github.com/rojo-rbx/rojo/pull/1030
|
||||||
|
[#1096]: https://github.com/rojo-rbx/rojo/pull/1096
|
||||||
|
[#1093]: https://github.com/rojo-rbx/rojo/pull/1093
|
||||||
|
[#1084]: https://github.com/rojo-rbx/rojo/pull/1084
|
||||||
|
[#1085]: https://github.com/rojo-rbx/rojo/pull/1085
|
||||||
|
[#1081]: https://github.com/rojo-rbx/rojo/pull/1081
|
||||||
|
[#1080]: https://github.com/rojo-rbx/rojo/pull/1080
|
||||||
|
[#1066]: https://github.com/rojo-rbx/rojo/pull/1066
|
||||||
|
[#1069]: https://github.com/rojo-rbx/rojo/pull/1069
|
||||||
|
[#1092]: https://github.com/rojo-rbx/rojo/pull/1092
|
||||||
|
[#1104]: https://github.com/rojo-rbx/rojo/pull/1104
|
||||||
|
|
||||||
|
## 7.5.1 - April 25th, 2025
|
||||||
|
* Fixed output spam related to `Instance.Capabilities` in the plugin
|
||||||
|
|
||||||
|
## 7.5.0 - April 25th, 2025
|
||||||
|
* Fixed an edge case that caused model pivots to not be built correctly in some cases ([#1027])
|
||||||
|
* Add `blockedPlaceIds` project config field to allow blocking place ids from being live synced ([#1021])
|
||||||
|
* Adds support for `.plugin.lua(u)` files - this applies the `Plugin` RunContext. ([#1008])
|
||||||
|
* Added support for Roblox's `Content` type. This replaces the old `Content` type with `ContentId` to reflect Roblox's change.
|
||||||
|
If you were previously using the fully-qualified syntax for `Content` you will need to switch it to `ContentId`.
|
||||||
|
* Added support for `Enum` attributes
|
||||||
|
* Significantly improved performance of `.rbxm` parsing
|
||||||
|
* Support for a `$schema` field in all special JSON files (`.project.json`, `.model.json`, and `.meta.json`) ([#974])
|
||||||
* Projects may now manually link `Ref` properties together using `Attributes`. ([#843])
|
* Projects may now manually link `Ref` properties together using `Attributes`. ([#843])
|
||||||
This has two parts: using `id` or `$id` in JSON files or a `Rojo_Target` attribute, an Instance
|
This has two parts: using `id` or `$id` in JSON files or a `Rojo_Target` attribute, an Instance
|
||||||
is given an ID. Then, that ID may be used elsewhere in the project to point to an Instance
|
is given an ID. Then, that ID may be used elsewhere in the project to point to an Instance
|
||||||
@@ -28,6 +70,8 @@
|
|||||||
* Added experimental setting for Auto Connect in playtests ([#840])
|
* Added experimental setting for Auto Connect in playtests ([#840])
|
||||||
* Improved settings UI ([#886])
|
* Improved settings UI ([#886])
|
||||||
* `Open Scripts Externally` option can now be changed while syncing ([#911])
|
* `Open Scripts Externally` option can now be changed while syncing ([#911])
|
||||||
|
* The sync reminder notification will now tell you what was last synced and when ([#987])
|
||||||
|
* Fixed notification and tooltip text sometimes getting cut off ([#988])
|
||||||
* Projects may now specify rules for syncing files as if they had a different file extension. ([#813])
|
* Projects may now specify rules for syncing files as if they had a different file extension. ([#813])
|
||||||
This is specified via a new field on project files, `syncRules`:
|
This is specified via a new field on project files, `syncRules`:
|
||||||
|
|
||||||
@@ -56,7 +100,7 @@
|
|||||||
|
|
||||||
Additionally, the `exclude` field allows files to be excluded from the sync rule if they match a pattern specified by it. If it's not present, all files that match `pattern` will be modified using the sync rule.
|
Additionally, the `exclude` field allows files to be excluded from the sync rule if they match a pattern specified by it. If it's not present, all files that match `pattern` will be modified using the sync rule.
|
||||||
|
|
||||||
The `use` field corresponds to one of the potential file type that Rojo will currently include in a project. Files that match the provided pattern will be treated as if they had the file extension for that file type. A full list is below:
|
The `use` field corresponds to one of the potential file type that Rojo will currently include in a project. Files that match the provided pattern will be treated as if they had the file extension for that file type.
|
||||||
|
|
||||||
| `use` value | file extension |
|
| `use` value | file extension |
|
||||||
|:---------------|:----------------|
|
|:---------------|:----------------|
|
||||||
@@ -73,6 +117,16 @@
|
|||||||
| `project` | `.project.json` |
|
| `project` | `.project.json` |
|
||||||
| `ignore` | None! |
|
| `ignore` | None! |
|
||||||
|
|
||||||
|
Additionally, there are `use` values for specific script types ([#909]):
|
||||||
|
|
||||||
|
| `use` value | script type |
|
||||||
|
|:-------------------------|:---------------------------------------|
|
||||||
|
| `legacyServerScript` | `Script` with `Enum.RunContext.Legacy` |
|
||||||
|
| `legacyClientScript` | `LocalScript` |
|
||||||
|
| `runContextServerScript` | `Script` with `Enum.RunContext.Server` |
|
||||||
|
| `runContextClientScript` | `Script` with `Enum.RunContext.Client` |
|
||||||
|
| `pluginScript` | `Script` with `Enum.RunContext.Plugin` |
|
||||||
|
|
||||||
**All** sync rules are reset between project files, so they must be specified in each one when nesting them. This is to ensure that nothing can break other projects by changing how files are synced!
|
**All** sync rules are reset between project files, so they must be specified in each one when nesting them. This is to ensure that nothing can break other projects by changing how files are synced!
|
||||||
|
|
||||||
[#813]: https://github.com/rojo-rbx/rojo/pull/813
|
[#813]: https://github.com/rojo-rbx/rojo/pull/813
|
||||||
@@ -83,8 +137,19 @@
|
|||||||
[#843]: https://github.com/rojo-rbx/rojo/pull/843
|
[#843]: https://github.com/rojo-rbx/rojo/pull/843
|
||||||
[#883]: https://github.com/rojo-rbx/rojo/pull/883
|
[#883]: https://github.com/rojo-rbx/rojo/pull/883
|
||||||
[#886]: https://github.com/rojo-rbx/rojo/pull/886
|
[#886]: https://github.com/rojo-rbx/rojo/pull/886
|
||||||
|
[#909]: https://github.com/rojo-rbx/rojo/pull/909
|
||||||
[#911]: https://github.com/rojo-rbx/rojo/pull/911
|
[#911]: https://github.com/rojo-rbx/rojo/pull/911
|
||||||
[#915]: https://github.com/rojo-rbx/rojo/pull/915
|
[#915]: https://github.com/rojo-rbx/rojo/pull/915
|
||||||
|
[#974]: https://github.com/rojo-rbx/rojo/pull/974
|
||||||
|
[#987]: https://github.com/rojo-rbx/rojo/pull/987
|
||||||
|
[#988]: https://github.com/rojo-rbx/rojo/pull/988
|
||||||
|
[#1008]: https://github.com/rojo-rbx/rojo/pull/1008
|
||||||
|
[#1021]: https://github.com/rojo-rbx/rojo/pull/1021
|
||||||
|
[#1027]: https://github.com/rojo-rbx/rojo/pull/1027
|
||||||
|
|
||||||
|
## [7.4.4] - August 22nd, 2024
|
||||||
|
* Fixed issue with reading attributes from `Lighting` in new place files
|
||||||
|
* `Instance.Archivable` will now default to `true` when building a project into a binary (`rbxm`/`rbxl`) file rather than `false`.
|
||||||
|
|
||||||
## [7.4.3] - August 6th, 2024
|
## [7.4.3] - August 6th, 2024
|
||||||
* Fixed issue with building binary files introduced in 7.4.2
|
* Fixed issue with building binary files introduced in 7.4.2
|
||||||
@@ -662,7 +727,7 @@ This is a general maintenance release for the Rojo 0.5.x release series.
|
|||||||
|
|
||||||
## [0.5.0 Alpha 11](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.11) (May 29, 2019)
|
## [0.5.0 Alpha 11](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.11) (May 29, 2019)
|
||||||
* Added support for implicit property values in JSON model files ([#154](https://github.com/rojo-rbx/rojo/pull/154))
|
* Added support for implicit property values in JSON model files ([#154](https://github.com/rojo-rbx/rojo/pull/154))
|
||||||
* `Content` propertyes can now be specified in projects and model files as regular string literals.
|
* `Content` properties can now be specified in projects and model files as regular string literals.
|
||||||
* Added support for `BrickColor` properties.
|
* Added support for `BrickColor` properties.
|
||||||
* Added support for properties added in client release 384, like `Lighting.Technology` being set to `"ShadowMap"`.
|
* Added support for properties added in client release 384, like `Lighting.Technology` being set to `"ShadowMap"`.
|
||||||
* Improved performance when working with XML models and places
|
* Improved performance when working with XML models and places
|
||||||
|
|||||||
@@ -15,12 +15,29 @@ You'll want these tools to work on Rojo:
|
|||||||
|
|
||||||
* Latest stable Rust compiler
|
* Latest stable Rust compiler
|
||||||
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
|
* 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
|
||||||
Documentation impacts way more people than the individual lines of code we write.
|
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
|
## 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.
|
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.
|
||||||
|
|||||||
229
Cargo.lock
generated
229
Cargo.lock
generated
@@ -17,6 +17,19 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
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]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -32,6 +45,12 @@ version = "1.0.80"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
|
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arraydeque"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayref"
|
name = "arrayref"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
@@ -171,6 +190,10 @@ name = "cc"
|
|||||||
version = "1.0.89"
|
version = "1.0.89"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723"
|
checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723"
|
||||||
|
dependencies = [
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
@@ -378,6 +401,12 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diff"
|
name = "diff"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
@@ -492,7 +521,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall 0.4.1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -502,6 +531,12 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -734,6 +769,24 @@ version = "0.14.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
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]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -935,6 +988,15 @@ version = "1.0.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jod-thread"
|
name = "jod-thread"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -962,9 +1024,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazycell"
|
name = "lazycell"
|
||||||
@@ -986,7 +1048,7 @@ checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.4.2",
|
"bitflags 2.4.2",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall 0.4.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1001,6 +1063,16 @@ version = "0.4.13"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
|
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]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.21"
|
version = "0.4.21"
|
||||||
@@ -1241,6 +1313,29 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
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]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
@@ -1310,6 +1405,12 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "plotters"
|
name = "plotters"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
@@ -1498,10 +1599,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_binary"
|
name = "rbx_binary"
|
||||||
version = "0.7.7"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b85057e8ff75a1ce99248200c4b3c7b481a3d52f921f1053ecd67921dcc7930"
|
checksum = "9573fee5e073d7b303f475c285197fdc8179468de66ca60ee115a58fbac99296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
"log",
|
"log",
|
||||||
"lz4",
|
"lz4",
|
||||||
"profiling",
|
"profiling",
|
||||||
@@ -1509,23 +1611,26 @@ dependencies = [
|
|||||||
"rbx_reflection",
|
"rbx_reflection",
|
||||||
"rbx_reflection_database",
|
"rbx_reflection_database",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_dom_weak"
|
name = "rbx_dom_weak"
|
||||||
version = "2.9.0"
|
version = "3.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fcd2a17d09e46af0805f8b311a926402172b97e8d9388745c9adf8f448901841"
|
checksum = "04425cf6e9376e5486f4fb35906c120d1b1b45618a490318cf563fab1fa230a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
"rbx_types",
|
"rbx_types",
|
||||||
"serde",
|
"serde",
|
||||||
|
"ustr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_reflection"
|
name = "rbx_reflection"
|
||||||
version = "4.7.0"
|
version = "5.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8118ac6021d700e8debe324af6b40ecfd2cef270a00247849dbdfeebb0802677"
|
checksum = "1b6d0d62baa613556b058a5f94a53b01cf0ccde0ea327ce03056e335b982e77e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rbx_types",
|
"rbx_types",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1534,9 +1639,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_reflection_database"
|
name = "rbx_reflection_database"
|
||||||
version = "0.2.12+roblox-638"
|
version = "1.0.3+roblox-670"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e29381d675420e841f8c02db5755cbb2545ed3e13f56c539546dc58702b512a"
|
checksum = "e22c05ef92528c0fb0cc580592a65ca178d3ea9beb07a1d9ca0a2503c4f3721c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"rbx_reflection",
|
"rbx_reflection",
|
||||||
@@ -1546,9 +1651,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_types"
|
name = "rbx_types"
|
||||||
version = "1.10.0"
|
version = "2.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e30f49b2a3bb667e4074ba73c2dfb8ca0873f610b448ccf318a240acfdec6c73"
|
checksum = "78e4fdde46493def107e5f923d82e813dec9b3eef52c2f75fbad3a716023eda2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
@@ -1561,10 +1666,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_xml"
|
name = "rbx_xml"
|
||||||
version = "0.13.5"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b14b3027bc9ccd82e2fc854c8bcd25ed58318e570c355bf2cf63df9cdbd5ba8"
|
checksum = "bb623833c31cc43bbdaeb32f5e91db8ecd63fc46e438d0d268baf9e61539cf1c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"log",
|
"log",
|
||||||
"rbx_dom_weak",
|
"rbx_dom_weak",
|
||||||
@@ -1582,6 +1688,15 @@ dependencies = [
|
|||||||
"bitflags 1.3.2",
|
"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]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@@ -1752,7 +1867,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "7.4.0"
|
version = "7.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
@@ -1761,6 +1876,7 @@ dependencies = [
|
|||||||
"criterion",
|
"criterion",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"csv",
|
"csv",
|
||||||
|
"data-encoding",
|
||||||
"embed-resource",
|
"embed-resource",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"fs-err",
|
"fs-err",
|
||||||
@@ -1800,6 +1916,7 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"winreg 0.10.1",
|
"winreg 0.10.1",
|
||||||
|
"yaml-rust2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1896,6 +2013,12 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sct"
|
name = "sct"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -2393,6 +2516,19 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"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]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.7.0"
|
version = "1.7.0"
|
||||||
@@ -2779,8 +2915,67 @@ dependencies = [
|
|||||||
"linked-hash-map",
|
"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]]
|
[[package]]
|
||||||
name = "yansi"
|
name = "yansi"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
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.78",
|
||||||
|
"quote 1.0.35",
|
||||||
|
"syn 2.0.52",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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",
|
||||||
|
]
|
||||||
|
|||||||
23
Cargo.toml
23
Cargo.toml
@@ -1,8 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "7.4.0"
|
version = "7.5.1"
|
||||||
rust-version = "1.70.0"
|
rust-version = "1.79.0"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
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"
|
description = "Enables professional-grade development tools for Roblox developers"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
homepage = "https://rojo.space"
|
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_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
||||||
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
||||||
|
|
||||||
rbx_binary = "0.7.7"
|
rbx_binary = "1.0.0"
|
||||||
rbx_dom_weak = "2.9.0"
|
rbx_dom_weak = "3.0.0"
|
||||||
rbx_reflection = "4.7.0"
|
rbx_reflection = "5.0.0"
|
||||||
rbx_reflection_database = "0.2.12"
|
rbx_reflection_database = "1.0.3"
|
||||||
rbx_xml = "0.13.5"
|
rbx_xml = "1.0.0"
|
||||||
|
|
||||||
anyhow = "1.0.80"
|
anyhow = "1.0.80"
|
||||||
backtrace = "0.3.69"
|
backtrace = "0.3.69"
|
||||||
@@ -70,7 +74,6 @@ humantime = "2.1.0"
|
|||||||
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
|
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
|
||||||
jod-thread = "0.1.2"
|
jod-thread = "0.1.2"
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
maplit = "1.0.2"
|
|
||||||
num_cpus = "1.16.0"
|
num_cpus = "1.16.0"
|
||||||
opener = "0.5.2"
|
opener = "0.5.2"
|
||||||
rayon = "1.9.0"
|
rayon = "1.9.0"
|
||||||
@@ -90,6 +93,8 @@ tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread"] }
|
|||||||
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
||||||
clap = { version = "3.2.25", features = ["derive"] }
|
clap = { version = "3.2.25", features = ["derive"] }
|
||||||
profiling = "1.0.15"
|
profiling = "1.0.15"
|
||||||
|
yaml-rust2 = "0.10.3"
|
||||||
|
data-encoding = "2.8.0"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.10.1"
|
winreg = "0.10.1"
|
||||||
|
|||||||
@@ -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;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #e7e7e7
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width:100%;
|
max-width:100%;
|
||||||
max-height:100%;
|
max-height:100%;
|
||||||
|
|||||||
28
build.rs
28
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();
|
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
|
// We can skip any TestEZ test files since they aren't necessary for
|
||||||
// the plugin to run.
|
// the plugin to run.
|
||||||
if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") {
|
if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") {
|
||||||
@@ -41,12 +45,12 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
|||||||
fn main() -> Result<(), anyhow::Error> {
|
fn main() -> Result<(), anyhow::Error> {
|
||||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||||
|
|
||||||
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
let root_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let plugin_root = PathBuf::from(root_dir).join("plugin");
|
let plugin_dir = root_dir.join("plugin");
|
||||||
|
|
||||||
let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?;
|
let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?;
|
||||||
let plugin_version =
|
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!(
|
assert_eq!(
|
||||||
our_version, plugin_version,
|
our_version, plugin_version,
|
||||||
@@ -54,14 +58,16 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let snapshot = VfsSnapshot::dir(hashmap! {
|
let snapshot = VfsSnapshot::dir(hashmap! {
|
||||||
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
|
"default.project.json" => snapshot_from_fs_path(&root_dir.join("plugin.project.json"))?,
|
||||||
"fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?,
|
"plugin" => VfsSnapshot::dir(hashmap! {
|
||||||
"http" => snapshot_from_fs_path(&plugin_root.join("http"))?,
|
"fmt" => snapshot_from_fs_path(&plugin_dir.join("fmt"))?,
|
||||||
"log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
|
"http" => snapshot_from_fs_path(&plugin_dir.join("http"))?,
|
||||||
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
|
"log" => snapshot_from_fs_path(&plugin_dir.join("log"))?,
|
||||||
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
|
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_dir.join("rbx_dom_lua"))?,
|
||||||
"Packages" => snapshot_from_fs_path(&plugin_root.join("Packages"))?,
|
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?,
|
||||||
"Version.txt" => snapshot_from_fs_path(&plugin_root.join("Version.txt"))?,
|
"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_path = Path::new(&out_dir).join("plugin.bincode");
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
name = "memofs"
|
name = "memofs"
|
||||||
description = "Virtual filesystem with configurable backends."
|
description = "Virtual filesystem with configurable backends."
|
||||||
version = "0.3.0"
|
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"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -5,19 +5,13 @@ use serde::Serialize;
|
|||||||
/// Enables redacting any value that serializes as a string.
|
/// Enables redacting any value that serializes as a string.
|
||||||
///
|
///
|
||||||
/// Used for transforming Rojo instance IDs into something deterministic.
|
/// Used for transforming Rojo instance IDs into something deterministic.
|
||||||
|
#[derive(Default)]
|
||||||
pub struct RedactionMap {
|
pub struct RedactionMap {
|
||||||
ids: HashMap<String, usize>,
|
ids: HashMap<String, usize>,
|
||||||
last_id: usize,
|
last_id: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RedactionMap {
|
impl RedactionMap {
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
ids: HashMap::new(),
|
|
||||||
last_id: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> {
|
pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> {
|
||||||
let id = id.to_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) {
|
pub fn intern(&mut self, id: impl ToString) {
|
||||||
let last_id = &mut self.last_id;
|
let last_id = &mut self.last_id;
|
||||||
|
|
||||||
|
|||||||
@@ -3,25 +3,25 @@
|
|||||||
"tree": {
|
"tree": {
|
||||||
"$className": "Folder",
|
"$className": "Folder",
|
||||||
"Plugin": {
|
"Plugin": {
|
||||||
"$path": "src"
|
"$path": "plugin/src"
|
||||||
},
|
},
|
||||||
"Packages": {
|
"Packages": {
|
||||||
"$path": "Packages",
|
"$path": "plugin/Packages",
|
||||||
"Log": {
|
"Log": {
|
||||||
"$path": "log"
|
"$path": "plugin/log"
|
||||||
},
|
},
|
||||||
"Http": {
|
"Http": {
|
||||||
"$path": "http"
|
"$path": "plugin/http"
|
||||||
},
|
},
|
||||||
"Fmt": {
|
"Fmt": {
|
||||||
"$path": "fmt"
|
"$path": "plugin/fmt"
|
||||||
},
|
},
|
||||||
"RbxDom": {
|
"RbxDom": {
|
||||||
"$path": "rbx_dom_lua"
|
"$path": "plugin/rbx_dom_lua"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Version": {
|
"Version": {
|
||||||
"$path": "Version.txt"
|
"$path": "plugin/Version.txt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Submodule plugin/Packages/t updated: 1f9754254b...1dbfccc182
@@ -1 +1 @@
|
|||||||
7.4.0
|
7.5.1
|
||||||
@@ -188,6 +188,38 @@ types = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Content = {
|
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,
|
fromPod = identity,
|
||||||
toPod = identity,
|
toPod = identity,
|
||||||
},
|
},
|
||||||
@@ -205,6 +237,19 @@ types = {
|
|||||||
end,
|
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 = {
|
Faces = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
local faces = {}
|
local faces = {}
|
||||||
@@ -300,7 +345,12 @@ types = {
|
|||||||
local keypoints = {}
|
local keypoints = {}
|
||||||
|
|
||||||
for index, keypoint in ipairs(pod.keypoints) do
|
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
|
end
|
||||||
|
|
||||||
return NumberSequence.new(keypoints)
|
return NumberSequence.new(keypoints)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Error.Kind = {
|
|||||||
UnknownProperty = "UnknownProperty",
|
UnknownProperty = "UnknownProperty",
|
||||||
PropertyNotReadable = "PropertyNotReadable",
|
PropertyNotReadable = "PropertyNotReadable",
|
||||||
PropertyNotWritable = "PropertyNotWritable",
|
PropertyNotWritable = "PropertyNotWritable",
|
||||||
|
CannotParseBinaryString = "CannotParseBinaryString",
|
||||||
Roblox = "Roblox",
|
Roblox = "Roblox",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,12 @@
|
|||||||
0.0
|
0.0
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"TestEnumItem": {
|
||||||
|
"EnumItem": {
|
||||||
|
"type": "Material",
|
||||||
|
"value": 256
|
||||||
|
}
|
||||||
|
},
|
||||||
"TestNumber": {
|
"TestNumber": {
|
||||||
"Float64": 1337.0
|
"Float64": 1337.0
|
||||||
},
|
},
|
||||||
@@ -170,9 +176,23 @@
|
|||||||
},
|
},
|
||||||
"ty": "ColorSequence"
|
"ty": "ColorSequence"
|
||||||
},
|
},
|
||||||
"Content": {
|
"ContentId": {
|
||||||
"value": {
|
"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"
|
"ty": "Content"
|
||||||
},
|
},
|
||||||
@@ -182,6 +202,15 @@
|
|||||||
},
|
},
|
||||||
"ty": "Enum"
|
"ty": "Enum"
|
||||||
},
|
},
|
||||||
|
"EnumItem": {
|
||||||
|
"value": {
|
||||||
|
"EnumItem": {
|
||||||
|
"type": "Material",
|
||||||
|
"value": 256
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ty": "EnumItem"
|
||||||
|
},
|
||||||
"Faces": {
|
"Faces": {
|
||||||
"value": {
|
"value": {
|
||||||
"Faces": [
|
"Faces": [
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
local CollectionService = game:GetService("CollectionService")
|
local CollectionService = game:GetService("CollectionService")
|
||||||
local ScriptEditorService = game:GetService("ScriptEditorService")
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
||||||
|
|
||||||
|
local Error = require(script.Parent.Error)
|
||||||
|
|
||||||
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
|
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
|
||||||
local TERRAIN_MATERIAL_COLORS = {
|
local TERRAIN_MATERIAL_COLORS = {
|
||||||
Enum.Material.Grass,
|
Enum.Material.Grass,
|
||||||
@@ -51,6 +53,10 @@ return {
|
|||||||
return true, instance:GetAttributes()
|
return true, instance:GetAttributes()
|
||||||
end,
|
end,
|
||||||
write = function(instance, _, value)
|
write = function(instance, _, value)
|
||||||
|
if typeof(value) ~= "table" then
|
||||||
|
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||||
|
end
|
||||||
|
|
||||||
local existing = instance:GetAttributes()
|
local existing = instance:GetAttributes()
|
||||||
local didAllWritesSucceed = true
|
local didAllWritesSucceed = true
|
||||||
|
|
||||||
@@ -160,9 +166,14 @@ return {
|
|||||||
return true, colors
|
return true, colors
|
||||||
end,
|
end,
|
||||||
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
|
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
|
for material, color in value do
|
||||||
instance:SetMaterialColor(material, color)
|
instance:SetMaterialColor(material, color)
|
||||||
end
|
end
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
|
||||||
local TestEZ = require(ReplicatedStorage.Packages.TestEZ)
|
local TestEZ = require(ReplicatedStorage.Packages:WaitForChild("TestEZ", 10))
|
||||||
|
|
||||||
local Rojo = ReplicatedStorage.Rojo
|
local Rojo = ReplicatedStorage.Rojo
|
||||||
|
|
||||||
@@ -8,4 +8,12 @@ local Settings = require(Rojo.Plugin.Settings)
|
|||||||
Settings:set("logLevel", "Trace")
|
Settings:set("logLevel", "Trace")
|
||||||
Settings:set("typecheckingEnabled", true)
|
Settings:set("typecheckingEnabled", true)
|
||||||
|
|
||||||
require(Rojo.Plugin.runTests)(TestEZ)
|
local results = require(Rojo.Plugin.runTests)(TestEZ)
|
||||||
|
|
||||||
|
-- Roblox's Luau execution gets mad about cyclical tables.
|
||||||
|
-- Rather than making TestEZ not do that, we just send back the important info.
|
||||||
|
return {
|
||||||
|
failureCount = results.failureCount,
|
||||||
|
successCount = results.successCount,
|
||||||
|
skippedCount = results.skippedCount,
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ local Version = require(script.Parent.Version)
|
|||||||
local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
|
local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
|
||||||
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
|
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
|
||||||
local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
|
local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
|
||||||
|
local validateApiSerialize = Types.ifEnabled(Types.ApiSerializeResponse)
|
||||||
|
local validateApiRefPatch = Types.ifEnabled(Types.ApiRefPatchResponse)
|
||||||
|
|
||||||
local function rejectFailedRequests(response)
|
local function rejectFailedRequests(response)
|
||||||
if response.code >= 400 then
|
if response.code >= 400 then
|
||||||
@@ -45,14 +47,7 @@ end
|
|||||||
|
|
||||||
local function rejectWrongPlaceId(infoResponseBody)
|
local function rejectWrongPlaceId(infoResponseBody)
|
||||||
if infoResponseBody.expectedPlaceIds ~= nil then
|
if infoResponseBody.expectedPlaceIds ~= nil then
|
||||||
local foundId = false
|
local foundId = table.find(infoResponseBody.expectedPlaceIds, game.PlaceId)
|
||||||
|
|
||||||
for _, id in ipairs(infoResponseBody.expectedPlaceIds) do
|
|
||||||
if id == game.PlaceId then
|
|
||||||
foundId = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not foundId then
|
if not foundId then
|
||||||
local idList = {}
|
local idList = {}
|
||||||
@@ -62,10 +57,30 @@ local function rejectWrongPlaceId(infoResponseBody)
|
|||||||
|
|
||||||
local message = (
|
local message = (
|
||||||
"Found a Rojo server, but its project is set to only be used with a specific list of places."
|
"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%s"
|
||||||
.. "\n\nTo change this list, edit 'servePlaceIds' in your .project.json file."
|
.. "\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)
|
return Promise.reject(message)
|
||||||
end
|
end
|
||||||
@@ -239,4 +254,32 @@ function ApiContext:open(id)
|
|||||||
end)
|
end)
|
||||||
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
|
return ApiContext
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ end
|
|||||||
|
|
||||||
function Checkbox:render()
|
function Checkbox:render()
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
theme = theme.Checkbox
|
local checkboxTheme = theme.Checkbox
|
||||||
|
|
||||||
local activeTransparency = Roact.joinBindings({
|
local activeTransparency = Roact.joinBindings({
|
||||||
self.binding:map(function(value)
|
self.binding:map(function(value)
|
||||||
@@ -57,20 +57,21 @@ function Checkbox:render()
|
|||||||
end,
|
end,
|
||||||
}, {
|
}, {
|
||||||
StateTip = e(Tooltip.Trigger, {
|
StateTip = e(Tooltip.Trigger, {
|
||||||
text = (if self.props.locked then "[LOCKED] " else "")
|
text = (if self.props.locked
|
||||||
.. (if self.props.active then "Enabled" else "Disabled"),
|
then (self.props.lockedTooltip or "(Cannot be changed right now)") .. "\n"
|
||||||
|
else "") .. (if self.props.active then "Enabled" else "Disabled"),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Active = e(SlicedImage, {
|
Active = e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = theme.Active.BackgroundColor,
|
color = checkboxTheme.Active.BackgroundColor,
|
||||||
transparency = activeTransparency,
|
transparency = activeTransparency,
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
zIndex = 2,
|
zIndex = 2,
|
||||||
}, {
|
}, {
|
||||||
Icon = e("ImageLabel", {
|
Icon = e("ImageLabel", {
|
||||||
Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Active,
|
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,
|
ImageTransparency = activeTransparency,
|
||||||
|
|
||||||
Size = UDim2.new(0, 16, 0, 16),
|
Size = UDim2.new(0, 16, 0, 16),
|
||||||
@@ -83,7 +84,7 @@ function Checkbox:render()
|
|||||||
|
|
||||||
Inactive = e(SlicedImage, {
|
Inactive = e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBorder,
|
slice = Assets.Slices.RoundedBorder,
|
||||||
color = theme.Inactive.BorderColor,
|
color = checkboxTheme.Inactive.BorderColor,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
}, {
|
}, {
|
||||||
@@ -91,7 +92,7 @@ function Checkbox:render()
|
|||||||
Image = if self.props.locked
|
Image = if self.props.locked
|
||||||
then Assets.Images.Checkbox.Locked
|
then Assets.Images.Checkbox.Locked
|
||||||
else Assets.Images.Checkbox.Inactive,
|
else Assets.Images.Checkbox.Inactive,
|
||||||
ImageColor3 = theme.Inactive.IconColor,
|
ImageColor3 = checkboxTheme.Inactive.IconColor,
|
||||||
ImageTransparency = self.props.transparency,
|
ImageTransparency = self.props.transparency,
|
||||||
|
|
||||||
Size = UDim2.new(0, 16, 0, 16),
|
Size = UDim2.new(0, 16, 0, 16),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
@@ -7,6 +8,8 @@ Highlighter.matchStudioSettings()
|
|||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
local CodeLabel = Roact.PureComponent:extend("CodeLabel")
|
local CodeLabel = Roact.PureComponent:extend("CodeLabel")
|
||||||
|
|
||||||
function CodeLabel:init()
|
function CodeLabel:init()
|
||||||
@@ -40,13 +43,14 @@ function CodeLabel:updateHighlights()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function CodeLabel:render()
|
function CodeLabel:render()
|
||||||
|
return Theme.with(function(theme)
|
||||||
return e("TextLabel", {
|
return e("TextLabel", {
|
||||||
Size = self.props.size,
|
Size = self.props.size,
|
||||||
Position = self.props.position,
|
Position = self.props.position,
|
||||||
Text = self.props.text,
|
Text = self.props.text,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.RobotoMono,
|
FontFace = theme.Font.Code,
|
||||||
TextSize = 16,
|
TextSize = theme.TextSize.Code,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextYAlignment = Enum.TextYAlignment.Top,
|
TextYAlignment = Enum.TextYAlignment.Top,
|
||||||
TextColor3 = Color3.fromRGB(255, 255, 255),
|
TextColor3 = Color3.fromRGB(255, 255, 255),
|
||||||
@@ -56,6 +60,7 @@ function CodeLabel:render()
|
|||||||
[Roact.Ref] = self.highlightsRef,
|
[Roact.Ref] = self.highlightsRef,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return CodeLabel
|
return CodeLabel
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
local TextService = game:GetService("TextService")
|
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
@@ -10,9 +8,11 @@ local Flipper = require(Packages.Flipper)
|
|||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||||
|
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||||
|
|
||||||
local SlicedImage = require(script.Parent.SlicedImage)
|
local SlicedImage = require(script.Parent.SlicedImage)
|
||||||
local ScrollingFrame = require(script.Parent.ScrollingFrame)
|
local ScrollingFrame = require(script.Parent.ScrollingFrame)
|
||||||
|
local Tooltip = require(script.Parent.Tooltip)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -44,29 +44,29 @@ end
|
|||||||
|
|
||||||
function Dropdown:render()
|
function Dropdown:render()
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
theme = theme.Dropdown
|
local dropdownTheme = theme.Dropdown
|
||||||
|
|
||||||
local optionButtons = {}
|
local optionButtons = {}
|
||||||
local width = -1
|
local width = -1
|
||||||
for i, option in self.props.options do
|
for i, option in self.props.options do
|
||||||
local text = tostring(option or "")
|
local text = tostring(option or "")
|
||||||
local textSize = TextService:GetTextSize(text, 15, Enum.Font.GothamMedium, Vector2.new(math.huge, 20))
|
local textBounds = getTextBoundsAsync(text, theme.Font.Main, theme.TextSize.Body, math.huge)
|
||||||
if textSize.X > width then
|
if textBounds.X > width then
|
||||||
width = textSize.X
|
width = textBounds.X
|
||||||
end
|
end
|
||||||
|
|
||||||
optionButtons[text] = e("TextButton", {
|
optionButtons[text] = e("TextButton", {
|
||||||
Text = text,
|
Text = text,
|
||||||
LayoutOrder = i,
|
LayoutOrder = i,
|
||||||
Size = UDim2.new(1, 0, 0, 24),
|
Size = UDim2.new(1, 0, 0, 24),
|
||||||
BackgroundColor3 = theme.BackgroundColor,
|
BackgroundColor3 = dropdownTheme.BackgroundColor,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
BackgroundTransparency = self.props.transparency,
|
BackgroundTransparency = self.props.transparency,
|
||||||
BorderSizePixel = 0,
|
BorderSizePixel = 0,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = dropdownTheme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Body,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
|
|
||||||
[Roact.Event.Activated] = function()
|
[Roact.Event.Activated] = function()
|
||||||
if self.props.locked then
|
if self.props.locked then
|
||||||
@@ -103,13 +103,13 @@ function Dropdown:render()
|
|||||||
}, {
|
}, {
|
||||||
Border = e(SlicedImage, {
|
Border = e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBorder,
|
slice = Assets.Slices.RoundedBorder,
|
||||||
color = theme.BorderColor,
|
color = dropdownTheme.BorderColor,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
}, {
|
}, {
|
||||||
DropArrow = e("ImageLabel", {
|
DropArrow = e("ImageLabel", {
|
||||||
Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow,
|
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,
|
ImageTransparency = self.props.transparency,
|
||||||
|
|
||||||
Size = UDim2.new(0, 18, 0, 18),
|
Size = UDim2.new(0, 18, 0, 18),
|
||||||
@@ -120,15 +120,21 @@ function Dropdown:render()
|
|||||||
end),
|
end),
|
||||||
|
|
||||||
BackgroundTransparency = 1,
|
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", {
|
Active = e("TextLabel", {
|
||||||
Size = UDim2.new(1, -30, 1, 0),
|
Size = UDim2.new(1, -30, 1, 0),
|
||||||
Position = UDim2.new(0, 6, 0, 0),
|
Position = UDim2.new(0, 6, 0, 0),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = self.props.active,
|
Text = self.props.active,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = dropdownTheme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
}),
|
}),
|
||||||
@@ -136,7 +142,7 @@ function Dropdown:render()
|
|||||||
Options = if self.state.open
|
Options = if self.state.open
|
||||||
then e(SlicedImage, {
|
then e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = theme.BackgroundColor,
|
color = dropdownTheme.BackgroundColor,
|
||||||
position = UDim2.new(1, 0, 1, 3),
|
position = UDim2.new(1, 0, 1, 3),
|
||||||
size = self.openBinding:map(function(a)
|
size = self.openBinding:map(function(a)
|
||||||
return UDim2.new(1, 0, a * math.min(3, #self.props.options), 0)
|
return UDim2.new(1, 0, a * math.min(3, #self.props.options), 0)
|
||||||
@@ -145,7 +151,7 @@ function Dropdown:render()
|
|||||||
}, {
|
}, {
|
||||||
Border = e(SlicedImage, {
|
Border = e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBorder,
|
slice = Assets.Slices.RoundedBorder,
|
||||||
color = theme.BorderColor,
|
color = dropdownTheme.BorderColor,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -9,8 +9,70 @@ local Assets = require(Plugin.Assets)
|
|||||||
local Config = require(Plugin.Config)
|
local Config = require(Plugin.Config)
|
||||||
local Version = require(Plugin.Version)
|
local Version = require(Plugin.Version)
|
||||||
|
|
||||||
|
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||||
|
local SlicedImage = require(script.Parent.SlicedImage)
|
||||||
|
|
||||||
local e = Roact.createElement
|
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)
|
local function Header(props)
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
return e("Frame", {
|
return e("Frame", {
|
||||||
@@ -29,18 +91,9 @@ local function Header(props)
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Version = e("TextLabel", {
|
VersionIndicator = e(VersionIndicator, {
|
||||||
Text = Version.display(Config.version),
|
transparency = props.transparency,
|
||||||
Font = Enum.Font.Gotham,
|
layoutOrder = 2,
|
||||||
TextSize = 14,
|
|
||||||
TextColor3 = theme.Header.VersionColor,
|
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
|
||||||
TextTransparency = props.transparency,
|
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 0, 14),
|
|
||||||
|
|
||||||
LayoutOrder = 2,
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Layout = e("UIListLayout", {
|
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 StudioService = game:GetService("StudioService")
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
@@ -9,16 +8,14 @@ local Roact = require(Packages.Roact)
|
|||||||
local Flipper = require(Packages.Flipper)
|
local Flipper = require(Packages.Flipper)
|
||||||
local Log = require(Packages.Log)
|
local Log = require(Packages.Log)
|
||||||
|
|
||||||
local bindingUtil = require(script.Parent.bindingUtil)
|
|
||||||
|
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local Assets = require(Plugin.Assets)
|
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 BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
local TextButton = require(Plugin.App.Components.TextButton)
|
local TextButton = require(Plugin.App.Components.TextButton)
|
||||||
|
|
||||||
local baseClock = DateTime.now().UnixTimestampMillis
|
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
local Notification = Roact.Component:extend("Notification")
|
local Notification = Roact.Component:extend("Notification")
|
||||||
@@ -78,7 +75,9 @@ function Notification:didMount()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Notification:willUnmount()
|
function Notification:willUnmount()
|
||||||
|
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
|
||||||
task.cancel(self.timeout)
|
task.cancel(self.timeout)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Notification:render()
|
function Notification:render()
|
||||||
@@ -86,8 +85,7 @@ function Notification:render()
|
|||||||
return 1 - value
|
return 1 - value
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local textBounds = TextService:GetTextSize(self.props.text, 15, Enum.Font.GothamMedium, Vector2.new(350, 700))
|
return Theme.with(function(theme)
|
||||||
|
|
||||||
local actionButtons = {}
|
local actionButtons = {}
|
||||||
local buttonsX = 0
|
local buttonsX = 0
|
||||||
if self.props.actions then
|
if self.props.actions then
|
||||||
@@ -97,21 +95,19 @@ function Notification:render()
|
|||||||
text = action.text,
|
text = action.text,
|
||||||
style = action.style,
|
style = action.style,
|
||||||
onClick = function()
|
onClick = function()
|
||||||
|
self:dismiss()
|
||||||
|
if action.onClick then
|
||||||
local success, err = pcall(action.onClick, self)
|
local success, err = pcall(action.onClick, self)
|
||||||
if not success then
|
if not success then
|
||||||
Log.warn("Error in notification action: " .. tostring(err))
|
Log.warn("Error in notification action: " .. tostring(err))
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end,
|
end,
|
||||||
layoutOrder = -action.layoutOrder,
|
layoutOrder = -action.layoutOrder,
|
||||||
transparency = transparency,
|
transparency = transparency,
|
||||||
})
|
})
|
||||||
|
|
||||||
buttonsX += TextService:GetTextSize(
|
buttonsX += getTextBoundsAsync(action.text, theme.Font.Main, theme.TextSize.Large, math.huge).X + (theme.TextSize.Body * 2)
|
||||||
action.text,
|
|
||||||
18,
|
|
||||||
Enum.Font.GothamMedium,
|
|
||||||
Vector2.new(math.huge, math.huge)
|
|
||||||
).X + 30
|
|
||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
end
|
end
|
||||||
@@ -120,7 +116,9 @@ function Notification:render()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local paddingY, logoSize = 20, 32
|
local paddingY, logoSize = 20, 32
|
||||||
local actionsY = if self.props.actions then 35 else 0
|
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 contentX = math.max(textBounds.X, buttonsX)
|
||||||
|
|
||||||
local size = self.binding:map(function(value)
|
local size = self.binding:map(function(value)
|
||||||
@@ -130,7 +128,6 @@ function Notification:render()
|
|||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
|
||||||
return e("TextButton", {
|
return e("TextButton", {
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Size = size,
|
Size = size,
|
||||||
@@ -144,31 +141,31 @@ function Notification:render()
|
|||||||
}, {
|
}, {
|
||||||
e(BorderedContainer, {
|
e(BorderedContainer, {
|
||||||
transparency = transparency,
|
transparency = transparency,
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.fromScale(1, 1),
|
||||||
}, {
|
}, {
|
||||||
Contents = e("Frame", {
|
Contents = e("Frame", {
|
||||||
Size = UDim2.new(0, 35 + contentX, 1, -paddingY),
|
Size = UDim2.fromScale(1, 1),
|
||||||
Position = UDim2.new(0, 0, 0, paddingY / 2),
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
Logo = e("ImageLabel", {
|
Logo = e("ImageLabel", {
|
||||||
ImageTransparency = transparency,
|
ImageTransparency = transparency,
|
||||||
Image = Assets.Images.PluginButton,
|
Image = Assets.Images.PluginButton,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Size = UDim2.new(0, logoSize, 0, logoSize),
|
Size = UDim2.fromOffset(logoSize, logoSize),
|
||||||
Position = UDim2.new(0, 0, 0, 0),
|
Position = UDim2.new(0, 0, 0, 0),
|
||||||
AnchorPoint = Vector2.new(0, 0),
|
AnchorPoint = Vector2.new(0, 0),
|
||||||
}),
|
}),
|
||||||
Info = e("TextLabel", {
|
Info = e("TextLabel", {
|
||||||
Text = self.props.text,
|
Text = self.props.text,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Notification.InfoColor,
|
TextColor3 = theme.Notification.InfoColor,
|
||||||
TextTransparency = transparency,
|
TextTransparency = transparency,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextYAlignment = Enum.TextYAlignment.Center,
|
||||||
TextWrapped = true,
|
TextWrapped = true,
|
||||||
|
|
||||||
Size = UDim2.new(0, textBounds.X, 0, textBounds.Y),
|
Size = UDim2.new(0, textBounds.X, 1, -actionsY),
|
||||||
Position = UDim2.fromOffset(35, 0),
|
Position = UDim2.fromOffset(35, 0),
|
||||||
|
|
||||||
LayoutOrder = 1,
|
LayoutOrder = 1,
|
||||||
@@ -176,8 +173,8 @@ function Notification:render()
|
|||||||
}),
|
}),
|
||||||
Actions = if self.props.actions
|
Actions = if self.props.actions
|
||||||
then e("Frame", {
|
then e("Frame", {
|
||||||
Size = UDim2.new(1, -40, 0, 35),
|
Size = UDim2.new(1, -40, 0, actionsY),
|
||||||
Position = UDim2.new(1, 0, 1, 0),
|
Position = UDim2.fromScale(1, 1),
|
||||||
AnchorPoint = Vector2.new(1, 1),
|
AnchorPoint = Vector2.new(1, 1),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
@@ -196,32 +193,12 @@ function Notification:render()
|
|||||||
Padding = e("UIPadding", {
|
Padding = e("UIPadding", {
|
||||||
PaddingLeft = UDim.new(0, 17),
|
PaddingLeft = UDim.new(0, 17),
|
||||||
PaddingRight = UDim.new(0, 15),
|
PaddingRight = UDim.new(0, 15),
|
||||||
|
PaddingTop = UDim.new(0, paddingY / 2),
|
||||||
|
PaddingBottom = UDim.new(0, paddingY / 2),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local Notifications = Roact.Component:extend("Notifications")
|
return Notification
|
||||||
|
|
||||||
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
|
|
||||||
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", {
|
Label = e("TextLabel", {
|
||||||
Text = "View Diff",
|
Text = "View Diff",
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -170,8 +170,8 @@ function ChangeList:render()
|
|||||||
ColumnA = e("TextLabel", {
|
ColumnA = e("TextLabel", {
|
||||||
Text = tostring(headerRow[1]),
|
Text = tostring(headerRow[1]),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -182,8 +182,8 @@ function ChangeList:render()
|
|||||||
ColumnB = e("TextLabel", {
|
ColumnB = e("TextLabel", {
|
||||||
Text = tostring(headerRow[2]),
|
Text = tostring(headerRow[2]),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -194,8 +194,8 @@ function ChangeList:render()
|
|||||||
ColumnC = e("TextLabel", {
|
ColumnC = e("TextLabel", {
|
||||||
Text = tostring(headerRow[3]),
|
Text = tostring(headerRow[3]),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -230,8 +230,8 @@ function ChangeList:render()
|
|||||||
ColumnA = e("TextLabel", {
|
ColumnA = e("TextLabel", {
|
||||||
Text = (if isWarning then "⚠ " else "") .. tostring(values[1]),
|
Text = (if isWarning then "⚠ " else "") .. tostring(values[1]),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = if isWarning then theme.Diff.Warning else theme.TextColor,
|
TextColor3 = if isWarning then theme.Diff.Warning else theme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ local function DisplayValue(props)
|
|||||||
Label = e("TextLabel", {
|
Label = e("TextLabel", {
|
||||||
Text = string.format("%d, %d, %d", props.value.R * 255, props.value.G * 255, props.value.B * 255),
|
Text = string.format("%d, %d, %d", props.value.R * 255, props.value.G * 255, props.value.B * 255),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = props.textColor,
|
TextColor3 = props.textColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -90,8 +90,8 @@ local function DisplayValue(props)
|
|||||||
return e("TextLabel", {
|
return e("TextLabel", {
|
||||||
Text = textRepresentation,
|
Text = textRepresentation,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = props.textColor,
|
TextColor3 = props.textColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -112,8 +112,8 @@ local function DisplayValue(props)
|
|||||||
return e("TextLabel", {
|
return e("TextLabel", {
|
||||||
Text = textRepresentation,
|
Text = textRepresentation,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = props.textColor,
|
TextColor3 = props.textColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ function DomLabel:render()
|
|||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
local color = if props.isWarning
|
local color = if props.isWarning
|
||||||
then theme.Diff.Warning
|
then theme.Diff.Warning
|
||||||
elseif props.patchType then theme.Diff[props.patchType]
|
elseif props.patchType then theme.Diff.Background[props.patchType]
|
||||||
else theme.TextColor
|
else theme.TextColor
|
||||||
|
|
||||||
local indent = (depth - 1) * 12 + 15
|
local indent = (depth - 1) * 12 + 15
|
||||||
@@ -225,8 +225,8 @@ function DomLabel:render()
|
|||||||
Text = (if props.isWarning then "⚠ " else "") .. props.name,
|
Text = (if props.isWarning then "⚠ " else "") .. props.name,
|
||||||
RichText = true,
|
RichText = true,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = if props.patchType then Enum.Font.GothamBold else Enum.Font.GothamMedium,
|
FontFace = if props.patchType then theme.Font.Bold else theme.Font.Main,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = color,
|
TextColor3 = color,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -251,11 +251,11 @@ function DomLabel:render()
|
|||||||
then e("TextLabel", {
|
then e("TextLabel", {
|
||||||
Text = props.changeInfo.edits .. if props.changeInfo.failed then "," else "",
|
Text = props.changeInfo.edits .. if props.changeInfo.failed then "," else "",
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.SubTextColor,
|
TextColor3 = theme.SubTextColor,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
Size = UDim2.new(0, 0, 0, 16),
|
Size = UDim2.new(0, 0, 0, theme.TextSize.Body),
|
||||||
AutomaticSize = Enum.AutomaticSize.X,
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
LayoutOrder = 2,
|
LayoutOrder = 2,
|
||||||
})
|
})
|
||||||
@@ -264,11 +264,11 @@ function DomLabel:render()
|
|||||||
then e("TextLabel", {
|
then e("TextLabel", {
|
||||||
Text = props.changeInfo.failed,
|
Text = props.changeInfo.failed,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Diff.Warning,
|
TextColor3 = theme.Diff.Warning,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
Size = UDim2.new(0, 0, 0, 16),
|
Size = UDim2.new(0, 0, 0, theme.TextSize.Body),
|
||||||
AutomaticSize = Enum.AutomaticSize.X,
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
LayoutOrder = 6,
|
LayoutOrder = 6,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -124,8 +124,8 @@ function PatchVisualizer:render()
|
|||||||
CleanMerge = e("TextLabel", {
|
CleanMerge = e("TextLabel", {
|
||||||
Visible = #scrollElements == 0,
|
Visible = #scrollElements == 0,
|
||||||
Text = "No changes to sync, project is up to date.",
|
Text = "No changes to sync, project is up to date.",
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Medium,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextWrapped = true,
|
TextWrapped = true,
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
local TextService = game:GetService("TextService")
|
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
@@ -11,6 +9,7 @@ local StringDiff = require(script:FindFirstChild("StringDiff"))
|
|||||||
|
|
||||||
local Timer = require(Plugin.Timer)
|
local Timer = require(Plugin.Timer)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||||
|
|
||||||
local CodeLabel = require(Plugin.App.Components.CodeLabel)
|
local CodeLabel = require(Plugin.App.Components.CodeLabel)
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
@@ -32,7 +31,6 @@ function StringDiffVisualizer:init()
|
|||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
self:calculateContentSize()
|
|
||||||
self:updateScriptBackground()
|
self:updateScriptBackground()
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
@@ -54,7 +52,6 @@ end
|
|||||||
|
|
||||||
function StringDiffVisualizer:didUpdate(previousProps)
|
function StringDiffVisualizer:didUpdate(previousProps)
|
||||||
if previousProps.oldString ~= self.props.oldString or previousProps.newString ~= self.props.newString then
|
if previousProps.oldString ~= self.props.oldString or previousProps.newString ~= self.props.newString then
|
||||||
self:calculateContentSize()
|
|
||||||
local add, remove = self:calculateDiffLines()
|
local add, remove = self:calculateDiffLines()
|
||||||
self:setState({
|
self:setState({
|
||||||
add = add,
|
add = add,
|
||||||
@@ -63,11 +60,11 @@ function StringDiffVisualizer:didUpdate(previousProps)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function StringDiffVisualizer:calculateContentSize()
|
function StringDiffVisualizer:calculateContentSize(theme)
|
||||||
local oldString, newString = self.props.oldString, self.props.newString
|
local oldString, newString = self.props.oldString, self.props.newString
|
||||||
|
|
||||||
local oldStringBounds = TextService:GetTextSize(oldString, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
|
local oldStringBounds = getTextBoundsAsync(oldString, theme.Font.Code, theme.TextSize.Code, math.huge)
|
||||||
local newStringBounds = TextService:GetTextSize(newString, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
|
local newStringBounds = getTextBoundsAsync(newString, theme.Font.Code, theme.TextSize.Code, math.huge)
|
||||||
|
|
||||||
self.setContentSize(
|
self.setContentSize(
|
||||||
Vector2.new(math.max(oldStringBounds.X, newStringBounds.X), math.max(oldStringBounds.Y, newStringBounds.Y))
|
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
|
local oldString, newString = self.props.oldString, self.props.newString
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
|
self:calculateContentSize(theme)
|
||||||
|
|
||||||
return e(BorderedContainer, {
|
return e(BorderedContainer, {
|
||||||
size = self.props.size,
|
size = self.props.size,
|
||||||
position = self.props.position,
|
position = self.props.position,
|
||||||
@@ -179,7 +178,7 @@ function StringDiffVisualizer:render()
|
|||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
position = UDim2.new(0, 0, 0, 0),
|
position = UDim2.new(0, 0, 0, 0),
|
||||||
text = oldString,
|
text = oldString,
|
||||||
lineBackground = theme.Diff.Remove,
|
lineBackground = theme.Diff.Background.Remove,
|
||||||
markedLines = self.state.remove,
|
markedLines = self.state.remove,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@@ -194,7 +193,7 @@ function StringDiffVisualizer:render()
|
|||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
position = UDim2.new(0, 0, 0, 0),
|
position = UDim2.new(0, 0, 0, 0),
|
||||||
text = newString,
|
text = newString,
|
||||||
lineBackground = theme.Diff.Add,
|
lineBackground = theme.Diff.Background.Add,
|
||||||
markedLines = self.state.add,
|
markedLines = self.state.add,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ function Array:render()
|
|||||||
e("Frame", {
|
e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 0, 25),
|
Size = UDim2.new(1, 0, 0, 25),
|
||||||
BackgroundTransparency = if patchType == "Remain" then 1 else self.props.transparency,
|
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,
|
BorderSizePixel = 0,
|
||||||
LayoutOrder = i,
|
LayoutOrder = i,
|
||||||
}, {
|
}, {
|
||||||
@@ -152,8 +152,8 @@ function Array:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = "Old",
|
Text = "Old",
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
}),
|
}),
|
||||||
@@ -163,8 +163,8 @@ function Array:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = "New",
|
Text = "New",
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -91,9 +91,7 @@ function Dictionary:render()
|
|||||||
LayoutOrder = order,
|
LayoutOrder = order,
|
||||||
BorderSizePixel = 0,
|
BorderSizePixel = 0,
|
||||||
BackgroundTransparency = if line.patchType == "Remain" then 1 else self.props.transparency,
|
BackgroundTransparency = if line.patchType == "Remain" then 1 else self.props.transparency,
|
||||||
BackgroundColor3 = if line.patchType == "Remain"
|
BackgroundColor3 = theme.Diff.Background[line.patchType],
|
||||||
then theme.Diff.Row
|
|
||||||
else theme.Diff[line.patchType],
|
|
||||||
}, {
|
}, {
|
||||||
DiffIcon = if line.patchType ~= "Remain"
|
DiffIcon = if line.patchType ~= "Remain"
|
||||||
then e("ImageLabel", {
|
then e("ImageLabel", {
|
||||||
@@ -112,9 +110,9 @@ function Dictionary:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = key,
|
Text = key,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.Diff.Text[line.patchType],
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
}),
|
}),
|
||||||
OldValue = e("Frame", {
|
OldValue = e("Frame", {
|
||||||
@@ -125,7 +123,7 @@ function Dictionary:render()
|
|||||||
e(DisplayValue, {
|
e(DisplayValue, {
|
||||||
value = oldValue,
|
value = oldValue,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
textColor = theme.Settings.Setting.DescriptionColor,
|
textColor = theme.Diff.Text[line.patchType],
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
NewValue = e("Frame", {
|
NewValue = e("Frame", {
|
||||||
@@ -136,7 +134,7 @@ function Dictionary:render()
|
|||||||
e(DisplayValue, {
|
e(DisplayValue, {
|
||||||
value = newValue,
|
value = newValue,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
textColor = theme.Settings.Setting.DescriptionColor,
|
textColor = theme.Diff.Text[line.patchType],
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -157,8 +155,8 @@ function Dictionary:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = "Key",
|
Text = "Key",
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
}),
|
}),
|
||||||
@@ -168,8 +166,8 @@ function Dictionary:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = "Old",
|
Text = "Old",
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
}),
|
}),
|
||||||
@@ -179,8 +177,8 @@ function Dictionary:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = "New",
|
Text = "New",
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ local Packages = Rojo.Packages
|
|||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
|
|
||||||
local SlicedImage = require(Plugin.App.Components.SlicedImage)
|
local SlicedImage = require(Plugin.App.Components.SlicedImage)
|
||||||
@@ -11,6 +12,7 @@ local SlicedImage = require(Plugin.App.Components.SlicedImage)
|
|||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
return function(props)
|
return function(props)
|
||||||
|
return Theme.with(function(theme)
|
||||||
return e(SlicedImage, {
|
return e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = props.color,
|
color = props.color,
|
||||||
@@ -20,7 +22,7 @@ return function(props)
|
|||||||
layoutOrder = props.layoutOrder,
|
layoutOrder = props.layoutOrder,
|
||||||
position = props.position,
|
position = props.position,
|
||||||
anchorPoint = props.anchorPoint,
|
anchorPoint = props.anchorPoint,
|
||||||
size = UDim2.new(0, 0, 0, 16),
|
size = UDim2.new(0, 0, 0, theme.TextSize.Medium),
|
||||||
automaticSize = Enum.AutomaticSize.X,
|
automaticSize = Enum.AutomaticSize.X,
|
||||||
}, {
|
}, {
|
||||||
Padding = e("UIPadding", {
|
Padding = e("UIPadding", {
|
||||||
@@ -42,8 +44,8 @@ return function(props)
|
|||||||
else nil,
|
else nil,
|
||||||
Text = e("TextLabel", {
|
Text = e("TextLabel", {
|
||||||
Text = props.text,
|
Text = props.text,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 12,
|
TextSize = theme.TextSize.Small,
|
||||||
TextColor3 = props.color,
|
TextColor3 = props.color,
|
||||||
TextXAlignment = Enum.TextXAlignment.Center,
|
TextXAlignment = Enum.TextXAlignment.Center,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -53,4 +55,5 @@ return function(props)
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
local TextService = game:GetService("TextService")
|
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
@@ -10,6 +8,7 @@ local Flipper = require(Packages.Flipper)
|
|||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||||
|
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||||
|
|
||||||
local SlicedImage = require(script.Parent.SlicedImage)
|
local SlicedImage = require(script.Parent.SlicedImage)
|
||||||
local TouchRipple = require(script.Parent.TouchRipple)
|
local TouchRipple = require(script.Parent.TouchRipple)
|
||||||
@@ -41,18 +40,17 @@ end
|
|||||||
|
|
||||||
function TextButton:render()
|
function TextButton:render()
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
local textSize =
|
local textBounds = getTextBoundsAsync(self.props.text, theme.Font.Main, theme.TextSize.Large, math.huge)
|
||||||
TextService:GetTextSize(self.props.text, 18, Enum.Font.GothamMedium, Vector2.new(math.huge, math.huge))
|
|
||||||
|
|
||||||
local style = self.props.style
|
local style = self.props.style
|
||||||
|
|
||||||
theme = theme.Button[style]
|
local buttonTheme = theme.Button[style]
|
||||||
|
|
||||||
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
|
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
|
||||||
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
|
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
|
||||||
|
|
||||||
return e("ImageButton", {
|
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,
|
Position = self.props.position,
|
||||||
AnchorPoint = self.props.anchorPoint,
|
AnchorPoint = self.props.anchorPoint,
|
||||||
|
|
||||||
@@ -74,18 +72,22 @@ function TextButton:render()
|
|||||||
end,
|
end,
|
||||||
}, {
|
}, {
|
||||||
TouchRipple = e(TouchRipple, {
|
TouchRipple = e(TouchRipple, {
|
||||||
color = theme.ActionFillColor,
|
color = buttonTheme.ActionFillColor,
|
||||||
transparency = self.props.transparency:map(function(value)
|
transparency = self.props.transparency:map(function(value)
|
||||||
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, value })
|
return bindingUtil.blendAlpha({ buttonTheme.ActionFillTransparency, value })
|
||||||
end),
|
end),
|
||||||
zIndex = 2,
|
zIndex = 2,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Text = e("TextLabel", {
|
Text = e("TextLabel", {
|
||||||
Text = self.props.text,
|
Text = self.props.text,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 18,
|
TextSize = theme.TextSize.Large,
|
||||||
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor),
|
TextColor3 = bindingUtil.mapLerp(
|
||||||
|
bindingEnabled,
|
||||||
|
buttonTheme.Enabled.TextColor,
|
||||||
|
buttonTheme.Disabled.TextColor
|
||||||
|
),
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
@@ -95,7 +97,11 @@ function TextButton:render()
|
|||||||
|
|
||||||
Border = style == "Bordered" and e(SlicedImage, {
|
Border = style == "Bordered" and e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBorder,
|
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,
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
@@ -105,14 +111,18 @@ function TextButton:render()
|
|||||||
|
|
||||||
HoverOverlay = e(SlicedImage, {
|
HoverOverlay = e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = theme.ActionFillColor,
|
color = buttonTheme.ActionFillColor,
|
||||||
transparency = Roact.joinBindings({
|
transparency = Roact.joinBindings({
|
||||||
hover = bindingHover:map(function(value)
|
hover = bindingHover:map(function(value)
|
||||||
return 1 - value
|
return 1 - value
|
||||||
end),
|
end),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
}):map(function(values)
|
}):map(function(values)
|
||||||
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
|
return bindingUtil.blendAlpha({
|
||||||
|
buttonTheme.ActionFillTransparency,
|
||||||
|
values.hover,
|
||||||
|
values.transparency,
|
||||||
|
})
|
||||||
end),
|
end),
|
||||||
|
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
@@ -124,8 +134,8 @@ function TextButton:render()
|
|||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = bindingUtil.mapLerp(
|
color = bindingUtil.mapLerp(
|
||||||
bindingEnabled,
|
bindingEnabled,
|
||||||
theme.Enabled.BackgroundColor,
|
buttonTheme.Enabled.BackgroundColor,
|
||||||
theme.Disabled.BackgroundColor
|
buttonTheme.Disabled.BackgroundColor
|
||||||
),
|
),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
|
|||||||
@@ -38,14 +38,18 @@ end
|
|||||||
|
|
||||||
function TextInput:render()
|
function TextInput:render()
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
theme = theme.TextInput
|
local textInputTheme = theme.TextInput
|
||||||
|
|
||||||
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
|
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
|
||||||
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
|
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
|
||||||
|
|
||||||
return e(SlicedImage, {
|
return e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBorder,
|
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,
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
size = self.props.size or UDim2.new(1, 0, 1, 0),
|
size = self.props.size or UDim2.new(1, 0, 1, 0),
|
||||||
@@ -55,14 +59,18 @@ function TextInput:render()
|
|||||||
}, {
|
}, {
|
||||||
HoverOverlay = e(SlicedImage, {
|
HoverOverlay = e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = theme.ActionFillColor,
|
color = textInputTheme.ActionFillColor,
|
||||||
transparency = Roact.joinBindings({
|
transparency = Roact.joinBindings({
|
||||||
hover = bindingHover:map(function(value)
|
hover = bindingHover:map(function(value)
|
||||||
return 1 - value
|
return 1 - value
|
||||||
end),
|
end),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
}):map(function(values)
|
}):map(function(values)
|
||||||
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
|
return bindingUtil.blendAlpha({
|
||||||
|
textInputTheme.ActionFillTransparency,
|
||||||
|
values.hover,
|
||||||
|
values.transparency,
|
||||||
|
})
|
||||||
end),
|
end),
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
zIndex = -1,
|
zIndex = -1,
|
||||||
@@ -72,14 +80,18 @@ function TextInput:render()
|
|||||||
Size = UDim2.fromScale(1, 1),
|
Size = UDim2.fromScale(1, 1),
|
||||||
Text = self.props.text,
|
Text = self.props.text,
|
||||||
PlaceholderText = self.props.placeholder,
|
PlaceholderText = self.props.placeholder,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Disabled.TextColor, theme.Enabled.TextColor),
|
TextColor3 = bindingUtil.mapLerp(
|
||||||
|
bindingEnabled,
|
||||||
|
textInputTheme.Disabled.TextColor,
|
||||||
|
textInputTheme.Enabled.TextColor
|
||||||
|
),
|
||||||
PlaceholderColor3 = bindingUtil.mapLerp(
|
PlaceholderColor3 = bindingUtil.mapLerp(
|
||||||
bindingEnabled,
|
bindingEnabled,
|
||||||
theme.Disabled.PlaceholderColor,
|
textInputTheme.Disabled.PlaceholderColor,
|
||||||
theme.Enabled.PlaceholderColor
|
textInputTheme.Enabled.PlaceholderColor
|
||||||
),
|
),
|
||||||
TextSize = 18,
|
TextSize = theme.TextSize.Large,
|
||||||
TextEditable = self.props.enabled,
|
TextEditable = self.props.enabled,
|
||||||
ClearTextOnFocus = self.props.clearTextOnFocus,
|
ClearTextOnFocus = self.props.clearTextOnFocus,
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
local TextService = game:GetService("TextService")
|
|
||||||
local HttpService = game:GetService("HttpService")
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
@@ -8,6 +7,8 @@ local Packages = Rojo.Packages
|
|||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
|
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||||
|
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
@@ -21,12 +22,10 @@ local Y_OVERLAP = 10 -- Let the triangle tail piece overlap the target a bit to
|
|||||||
local TooltipContext = Roact.createContext({})
|
local TooltipContext = Roact.createContext({})
|
||||||
|
|
||||||
local function Popup(props)
|
local function Popup(props)
|
||||||
local textSize = TextService:GetTextSize(
|
return Theme.with(function(theme)
|
||||||
props.Text,
|
local textXSpace = math.min(props.parentSize.X, 250) - TEXT_PADDING.X
|
||||||
16,
|
local textBounds = getTextBoundsAsync(props.Text, theme.Font.Main, theme.TextSize.Medium, textXSpace)
|
||||||
Enum.Font.GothamMedium,
|
local contentSize = textBounds + TEXT_PADDING + (Vector2.one * 2)
|
||||||
Vector2.new(math.min(props.parentSize.X, 160), math.huge)
|
|
||||||
) + TEXT_PADDING + (Vector2.one * 2)
|
|
||||||
|
|
||||||
local trigger = props.Trigger:getValue()
|
local trigger = props.Trigger:getValue()
|
||||||
|
|
||||||
@@ -35,36 +34,36 @@ local function Popup(props)
|
|||||||
local spaceAbove = trigger.AbsolutePosition.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
|
-- 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 displayAbove = spaceBelow < contentSize.Y and spaceAbove > spaceBelow
|
||||||
|
|
||||||
local X = math.clamp(props.Position.X - X_OFFSET, 0, props.parentSize.X - textSize.X)
|
local X = math.clamp(props.Position.X - X_OFFSET, 0, math.max(props.parentSize.X - contentSize.X, 1))
|
||||||
local Y = 0
|
local Y = 0
|
||||||
|
|
||||||
if displayAbove then
|
if displayAbove then
|
||||||
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - textSize.Y + Y_OVERLAP, 0)
|
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - contentSize.Y + Y_OVERLAP, 0)
|
||||||
else
|
else
|
||||||
Y = math.min(
|
Y = math.min(
|
||||||
trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP,
|
trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP,
|
||||||
props.parentSize.Y - textSize.Y
|
props.parentSize.Y - contentSize.Y
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
|
||||||
return e(BorderedContainer, {
|
return e(BorderedContainer, {
|
||||||
position = UDim2.fromOffset(X, Y),
|
position = UDim2.fromOffset(X, Y),
|
||||||
size = UDim2.fromOffset(textSize.X, textSize.Y),
|
size = UDim2.fromOffset(contentSize.X, contentSize.Y),
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
}, {
|
}, {
|
||||||
Label = e("TextLabel", {
|
Label = e("TextLabel", {
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Position = UDim2.fromScale(0.5, 0.5),
|
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),
|
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||||
|
Size = UDim2.fromOffset(textBounds.X, textBounds.Y),
|
||||||
Text = props.Text,
|
Text = props.Text,
|
||||||
TextSize = 16,
|
TextSize = theme.TextSize.Medium,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextWrapped = true,
|
TextWrapped = true,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextYAlignment = Enum.TextYAlignment.Center,
|
||||||
TextColor3 = theme.Button.Bordered.Enabled.TextColor,
|
TextColor3 = theme.Button.Bordered.Enabled.TextColor,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
}),
|
}),
|
||||||
@@ -72,8 +71,8 @@ local function Popup(props)
|
|||||||
Tail = e("ImageLabel", {
|
Tail = e("ImageLabel", {
|
||||||
ZIndex = 100,
|
ZIndex = 100,
|
||||||
Position = if displayAbove
|
Position = if displayAbove
|
||||||
then UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 1, -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, textSize.X - 6), 0, -TAIL_SIZE + 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),
|
Size = UDim2.fromOffset(TAIL_SIZE, TAIL_SIZE),
|
||||||
AnchorPoint = Vector2.new(0.5, 0),
|
AnchorPoint = Vector2.new(0.5, 0),
|
||||||
Rotation = if displayAbove then 180 else 0,
|
Rotation = if displayAbove then 180 else 0,
|
||||||
@@ -217,7 +216,7 @@ function Trigger:managePopup()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
self.showDelayThread = task.delay(DELAY, function()
|
self.showDelayThread = task.delay(self.props.delay or DELAY, function()
|
||||||
self.props.context.addTip(self.id, {
|
self.props.context.addTip(self.id, {
|
||||||
Text = self.props.text,
|
Text = self.props.text,
|
||||||
Position = self:getMousePos(),
|
Position = self:getMousePos(),
|
||||||
|
|||||||
@@ -64,13 +64,13 @@ function ConfirmingPage:render()
|
|||||||
"Sync changes for project '%s':",
|
"Sync changes for project '%s':",
|
||||||
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
|
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
|
||||||
),
|
),
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
LineHeight = 1.2,
|
LineHeight = 1.2,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
Size = UDim2.new(1, 0, 0, 20),
|
Size = UDim2.new(1, 0, 0, theme.TextSize.Large + 2),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -61,12 +61,12 @@ function ChangesViewer:render()
|
|||||||
|
|
||||||
Title = e("TextLabel", {
|
Title = e("TextLabel", {
|
||||||
Text = "Sync",
|
Text = "Sync",
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 17,
|
TextSize = theme.TextSize.Large,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextTransparency = self.props.transparency,
|
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),
|
Position = UDim2.new(0, 40, 0, 0),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
@@ -74,13 +74,13 @@ function ChangesViewer:render()
|
|||||||
Subtitle = e("TextLabel", {
|
Subtitle = e("TextLabel", {
|
||||||
Text = DateTime.fromUnixTimestamp(self.props.patchData.timestamp):FormatLocalTime("LTS", "en-us"),
|
Text = DateTime.fromUnixTimestamp(self.props.patchData.timestamp):FormatLocalTime("LTS", "en-us"),
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Medium,
|
||||||
TextColor3 = theme.SubTextColor,
|
TextColor3 = theme.SubTextColor,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
Size = UDim2.new(1, -40, 0, 16),
|
Size = UDim2.new(1, -40, 0, theme.TextSize.Medium),
|
||||||
Position = UDim2.new(0, 40, 0, 20),
|
Position = UDim2.new(0, 40, 0, theme.TextSize.Large + 2),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -131,8 +131,8 @@ function ChangesViewer:render()
|
|||||||
}),
|
}),
|
||||||
AppliedText = e("TextLabel", {
|
AppliedText = e("TextLabel", {
|
||||||
Text = applied,
|
Text = applied,
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
Size = UDim2.new(0, 0, 1, 0),
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
@@ -156,8 +156,8 @@ function ChangesViewer:render()
|
|||||||
}),
|
}),
|
||||||
UnappliedText = e("TextLabel", {
|
UnappliedText = e("TextLabel", {
|
||||||
Text = unapplied,
|
Text = unapplied,
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Diff.Warning,
|
TextColor3 = theme.Diff.Warning,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
Size = UDim2.new(0, 0, 1, 0),
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
@@ -217,13 +217,13 @@ local function ConnectionDetails(props)
|
|||||||
}, {
|
}, {
|
||||||
ProjectName = e("TextLabel", {
|
ProjectName = e("TextLabel", {
|
||||||
Text = props.projectName,
|
Text = props.projectName,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 20,
|
TextSize = theme.TextSize.Large,
|
||||||
TextColor3 = theme.ConnectionDetails.ProjectNameColor,
|
TextColor3 = theme.ConnectionDetails.ProjectNameColor,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 0, 20),
|
Size = UDim2.new(1, 0, 0, theme.TextSize.Large),
|
||||||
|
|
||||||
LayoutOrder = 1,
|
LayoutOrder = 1,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
@@ -231,13 +231,13 @@ local function ConnectionDetails(props)
|
|||||||
|
|
||||||
Address = e("TextLabel", {
|
Address = e("TextLabel", {
|
||||||
Text = props.address,
|
Text = props.address,
|
||||||
Font = Enum.Font.Code,
|
FontFace = theme.Font.Code,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Medium,
|
||||||
TextColor3 = theme.ConnectionDetails.AddressColor,
|
TextColor3 = theme.ConnectionDetails.AddressColor,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 0, 15),
|
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium),
|
||||||
|
|
||||||
LayoutOrder = 2,
|
LayoutOrder = 2,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
@@ -410,8 +410,8 @@ function ConnectedPage:render()
|
|||||||
Text = e("TextLabel", {
|
Text = e("TextLabel", {
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = self.changeInfoText,
|
Text = self.changeInfoText,
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
|
TextColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
TextXAlignment = Enum.TextXAlignment.Right,
|
TextXAlignment = Enum.TextXAlignment.Right,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
local TextService = game:GetService("TextService")
|
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
@@ -7,8 +5,10 @@ local Packages = Rojo.Packages
|
|||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||||
|
|
||||||
local TextButton = require(Plugin.App.Components.TextButton)
|
local TextButton = require(Plugin.App.Components.TextButton)
|
||||||
|
local Header = require(Plugin.App.Components.Header)
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||||
@@ -24,6 +24,7 @@ function Error:init()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Error:render()
|
function Error:render()
|
||||||
|
return Theme.with(function(theme)
|
||||||
return e(BorderedContainer, {
|
return e(BorderedContainer, {
|
||||||
size = Roact.joinBindings({
|
size = Roact.joinBindings({
|
||||||
containerSize = self.props.containerSize,
|
containerSize = self.props.containerSize,
|
||||||
@@ -38,6 +39,7 @@ function Error:render()
|
|||||||
return UDim2.new(1, 0, 0, math.min(outerSize.Y, maximumSize.Y))
|
return UDim2.new(1, 0, 0, math.min(outerSize.Y, maximumSize.Y))
|
||||||
end),
|
end),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = self.props.layoutOrder,
|
||||||
}, {
|
}, {
|
||||||
ScrollingFrame = e(ScrollingFrame, {
|
ScrollingFrame = e(ScrollingFrame, {
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
@@ -49,18 +51,17 @@ function Error:render()
|
|||||||
[Roact.Change.AbsoluteSize] = function(object)
|
[Roact.Change.AbsoluteSize] = function(object)
|
||||||
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
|
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
|
||||||
|
|
||||||
local textBounds = TextService:GetTextSize(
|
local textBounds = getTextBoundsAsync(
|
||||||
self.props.errorMessage,
|
self.props.errorMessage,
|
||||||
16,
|
theme.Font.Code,
|
||||||
Enum.Font.Code,
|
theme.TextSize.Code,
|
||||||
Vector2.new(containerSize.X, math.huge)
|
containerSize.X
|
||||||
)
|
)
|
||||||
|
|
||||||
self.setContentSize(Vector2.new(containerSize.X, textBounds.Y))
|
self.setContentSize(Vector2.new(containerSize.X, textBounds.Y))
|
||||||
end,
|
end,
|
||||||
}, {
|
}, {
|
||||||
ErrorMessage = Theme.with(function(theme)
|
ErrorMessage = e("TextBox", {
|
||||||
return e("TextBox", {
|
|
||||||
[Roact.Event.InputBegan] = function(rbx, input)
|
[Roact.Event.InputBegan] = function(rbx, input)
|
||||||
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
|
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
|
||||||
return
|
return
|
||||||
@@ -71,8 +72,8 @@ function Error:render()
|
|||||||
|
|
||||||
Text = self.props.errorMessage,
|
Text = self.props.errorMessage,
|
||||||
TextEditable = false,
|
TextEditable = false,
|
||||||
Font = Enum.Font.Code,
|
FontFace = theme.Font.Code,
|
||||||
TextSize = 16,
|
TextSize = theme.TextSize.Code,
|
||||||
TextColor3 = theme.ErrorColor,
|
TextColor3 = theme.ErrorColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextYAlignment = Enum.TextYAlignment.Top,
|
TextYAlignment = Enum.TextYAlignment.Top,
|
||||||
@@ -81,8 +82,7 @@ function Error:render()
|
|||||||
ClearTextOnFocus = false,
|
ClearTextOnFocus = false,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
})
|
}),
|
||||||
end),
|
|
||||||
|
|
||||||
Padding = e("UIPadding", {
|
Padding = e("UIPadding", {
|
||||||
PaddingLeft = UDim.new(0, ERROR_PADDING.X),
|
PaddingLeft = UDim.new(0, ERROR_PADDING.X),
|
||||||
@@ -92,6 +92,7 @@ function Error:render()
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local ErrorPage = Roact.Component:extend("ErrorPage")
|
local ErrorPage = Roact.Component:extend("ErrorPage")
|
||||||
@@ -109,16 +110,21 @@ function ErrorPage:render()
|
|||||||
self.setContainerSize(object.AbsoluteSize)
|
self.setContainerSize(object.AbsoluteSize)
|
||||||
end,
|
end,
|
||||||
}, {
|
}, {
|
||||||
Error = e(Error, {
|
Header = e(Header, {
|
||||||
errorMessage = self.state.errorMessage,
|
|
||||||
containerSize = self.containerSize,
|
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 1,
|
layoutOrder = 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
Error = e(Error, {
|
||||||
|
errorMessage = self.state.errorMessage,
|
||||||
|
containerSize = self.containerSize,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = 2,
|
||||||
|
}),
|
||||||
|
|
||||||
Buttons = e("Frame", {
|
Buttons = e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 0, 35),
|
Size = UDim2.new(1, 0, 0, 35),
|
||||||
LayoutOrder = 2,
|
LayoutOrder = 3,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
Close = e(TextButton, {
|
Close = e(TextButton, {
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ local function AddressEntry(props)
|
|||||||
}, {
|
}, {
|
||||||
Host = e("TextBox", {
|
Host = e("TextBox", {
|
||||||
Text = props.host or "",
|
Text = props.host or "",
|
||||||
Font = Enum.Font.Code,
|
FontFace = theme.Font.Code,
|
||||||
TextSize = 18,
|
TextSize = theme.TextSize.Large,
|
||||||
TextColor3 = theme.AddressEntry.TextColor,
|
TextColor3 = theme.AddressEntry.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -51,8 +51,8 @@ local function AddressEntry(props)
|
|||||||
|
|
||||||
Port = e("TextBox", {
|
Port = e("TextBox", {
|
||||||
Text = props.port or "",
|
Text = props.port or "",
|
||||||
Font = Enum.Font.Code,
|
FontFace = theme.Font.Code,
|
||||||
TextSize = 18,
|
TextSize = theme.TextSize.Large,
|
||||||
TextColor3 = theme.AddressEntry.TextColor,
|
TextColor3 = theme.AddressEntry.TextColor,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
PlaceholderText = Config.defaultPort,
|
PlaceholderText = Config.defaultPort,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
local TextService = game:GetService("TextService")
|
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
@@ -9,6 +7,7 @@ local Roact = require(Packages.Roact)
|
|||||||
local Settings = require(Plugin.Settings)
|
local Settings = require(Plugin.Settings)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||||
|
|
||||||
local Checkbox = require(Plugin.App.Components.Checkbox)
|
local Checkbox = require(Plugin.App.Components.Checkbox)
|
||||||
local Dropdown = require(Plugin.App.Components.Dropdown)
|
local Dropdown = require(Plugin.App.Components.Dropdown)
|
||||||
@@ -31,10 +30,16 @@ local TAG_TYPES = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
local function getTextBounds(text, textSize, font, lineHeight, bounds)
|
local function getTextBoundsWithLineHeight(
|
||||||
local textBounds = TextService:GetTextSize(text, textSize, font, bounds)
|
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
|
local lineHeightAbsolute = textSize * lineHeight
|
||||||
|
|
||||||
return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize))
|
return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize))
|
||||||
@@ -109,6 +114,7 @@ function Setting:render()
|
|||||||
then self.props.input
|
then self.props.input
|
||||||
elseif self.props.options ~= nil then e(Dropdown, {
|
elseif self.props.options ~= nil then e(Dropdown, {
|
||||||
locked = self.props.locked,
|
locked = self.props.locked,
|
||||||
|
lockedTooltip = self.props.lockedTooltip,
|
||||||
options = self.props.options,
|
options = self.props.options,
|
||||||
active = self.state.setting,
|
active = self.state.setting,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
@@ -118,6 +124,7 @@ function Setting:render()
|
|||||||
})
|
})
|
||||||
else e(Checkbox, {
|
else e(Checkbox, {
|
||||||
locked = self.props.locked,
|
locked = self.props.locked,
|
||||||
|
lockedTooltip = self.props.lockedTooltip,
|
||||||
active = self.state.setting,
|
active = self.state.setting,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
onClick = function()
|
onClick = function()
|
||||||
@@ -145,7 +152,7 @@ function Setting:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
Heading = e("Frame", {
|
Heading = e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 0, 16),
|
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
Layout = e("UIListLayout", {
|
Layout = e("UIListLayout", {
|
||||||
@@ -165,8 +172,8 @@ function Setting:render()
|
|||||||
else nil,
|
else nil,
|
||||||
Name = e("TextLabel", {
|
Name = e("TextLabel", {
|
||||||
Text = self.props.name,
|
Text = self.props.name,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 16,
|
TextSize = theme.TextSize.Medium,
|
||||||
TextColor3 = if self.props.tag and TAG_TYPES[self.props.tag]
|
TextColor3 = if self.props.tag and TAG_TYPES[self.props.tag]
|
||||||
then getThemeColorFromPath(theme, TAG_TYPES[self.props.tag].color)
|
then getThemeColorFromPath(theme, TAG_TYPES[self.props.tag].color)
|
||||||
else settingsTheme.Setting.NameColor,
|
else settingsTheme.Setting.NameColor,
|
||||||
@@ -174,7 +181,7 @@ function Setting:render()
|
|||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
RichText = true,
|
RichText = true,
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 0, 16),
|
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium),
|
||||||
|
|
||||||
LayoutOrder = 2,
|
LayoutOrder = 2,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
@@ -183,9 +190,9 @@ function Setting:render()
|
|||||||
|
|
||||||
Description = e("TextLabel", {
|
Description = e("TextLabel", {
|
||||||
Text = self.props.description,
|
Text = self.props.description,
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Main,
|
||||||
LineHeight = 1.2,
|
LineHeight = 1.2,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = settingsTheme.Setting.DescriptionColor,
|
TextColor3 = settingsTheme.Setting.DescriptionColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
@@ -197,12 +204,12 @@ function Setting:render()
|
|||||||
inputSize = self.inputSize,
|
inputSize = self.inputSize,
|
||||||
}):map(function(values)
|
}):map(function(values)
|
||||||
local offset = values.inputSize.X + 5
|
local offset = values.inputSize.X + 5
|
||||||
local textBounds = getTextBounds(
|
local textBounds = getTextBoundsWithLineHeight(
|
||||||
self.props.description,
|
self.props.description,
|
||||||
14,
|
theme.Font.Main,
|
||||||
Enum.Font.Gotham,
|
theme.TextSize.Body,
|
||||||
1.2,
|
values.containerSize.X - offset,
|
||||||
Vector2.new(values.containerSize.X - offset, math.huge)
|
1.2
|
||||||
)
|
)
|
||||||
return UDim2.new(1, -offset, 0, textBounds.Y)
|
return UDim2.new(1, -offset, 0, textBounds.Y)
|
||||||
end),
|
end),
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ end
|
|||||||
|
|
||||||
local invertedLevels = invertTbl(Log.Level)
|
local invertedLevels = invertTbl(Log.Level)
|
||||||
local confirmationBehaviors = { "Initial", "Always", "Large Changes", "Unlisted PlaceId", "Never" }
|
local confirmationBehaviors = { "Initial", "Always", "Large Changes", "Unlisted PlaceId", "Never" }
|
||||||
|
local syncReminderModes = { "None", "Notify", "Fullscreen" }
|
||||||
|
|
||||||
local function Navbar(props)
|
local function Navbar(props)
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
theme = theme.Settings.Navbar
|
local navbarTheme = theme.Settings.Navbar
|
||||||
|
|
||||||
return e("Frame", {
|
return e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 0, 46),
|
Size = UDim2.new(1, 0, 0, 46),
|
||||||
@@ -40,7 +41,7 @@ local function Navbar(props)
|
|||||||
Back = e(IconButton, {
|
Back = e(IconButton, {
|
||||||
icon = Assets.Images.Icons.Back,
|
icon = Assets.Images.Icons.Back,
|
||||||
iconSize = 24,
|
iconSize = 24,
|
||||||
color = theme.BackButtonColor,
|
color = navbarTheme.BackButtonColor,
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
|
|
||||||
position = UDim2.new(0, 0, 0.5, 0),
|
position = UDim2.new(0, 0, 0.5, 0),
|
||||||
@@ -55,9 +56,9 @@ local function Navbar(props)
|
|||||||
|
|
||||||
Text = e("TextLabel", {
|
Text = e("TextLabel", {
|
||||||
Text = "Settings",
|
Text = "Settings",
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
TextSize = 18,
|
TextSize = theme.TextSize.Large,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = navbarTheme.TextColor,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
@@ -81,9 +82,6 @@ function SettingsPage:render()
|
|||||||
return layoutOrder
|
return layoutOrder
|
||||||
end
|
end
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
|
||||||
theme = theme.Settings
|
|
||||||
|
|
||||||
return Roact.createFragment({
|
return Roact.createFragment({
|
||||||
Navbar = e(Navbar, {
|
Navbar = e(Navbar, {
|
||||||
onBack = self.props.onBack,
|
onBack = self.props.onBack,
|
||||||
@@ -96,6 +94,14 @@ function SettingsPage:render()
|
|||||||
contentSize = self.contentSize,
|
contentSize = self.contentSize,
|
||||||
transparency = self.props.transparency,
|
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(),
|
||||||
|
}),
|
||||||
|
|
||||||
ShowNotifications = e(Setting, {
|
ShowNotifications = e(Setting, {
|
||||||
id = "showNotifications",
|
id = "showNotifications",
|
||||||
name = "Show Notifications",
|
name = "Show Notifications",
|
||||||
@@ -104,13 +110,26 @@ function SettingsPage:render()
|
|||||||
layoutOrder = layoutIncrement(),
|
layoutOrder = layoutIncrement(),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
SyncReminder = e(Setting, {
|
SyncReminderMode = e(Setting, {
|
||||||
id = "syncReminder",
|
id = "syncReminderMode",
|
||||||
name = "Sync Reminder",
|
name = "Sync Reminder",
|
||||||
description = "Notify to sync when opening a place that has previously been synced",
|
description = "What type of reminders you receive for syncing your project",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
visible = Settings:getBinding("showNotifications"),
|
|
||||||
layoutOrder = layoutIncrement(),
|
layoutOrder = layoutIncrement(),
|
||||||
|
visible = Settings:getBinding("showNotifications"),
|
||||||
|
|
||||||
|
options = syncReminderModes,
|
||||||
|
}),
|
||||||
|
|
||||||
|
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),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ConfirmationBehavior = e(Setting, {
|
ConfirmationBehavior = e(Setting, {
|
||||||
@@ -162,6 +181,14 @@ function SettingsPage:render()
|
|||||||
layoutOrder = layoutIncrement(),
|
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, {
|
CheckForUpdates = e(Setting, {
|
||||||
id = "checkForUpdates",
|
id = "checkForUpdates",
|
||||||
name = "Check For Updates",
|
name = "Check For Updates",
|
||||||
@@ -204,6 +231,7 @@ function SettingsPage:render()
|
|||||||
name = "Two-Way Sync",
|
name = "Two-Way Sync",
|
||||||
description = "Editing files in Studio will sync them into the filesystem",
|
description = "Editing files in Studio will sync them into the filesystem",
|
||||||
locked = self.props.syncActive,
|
locked = self.props.syncActive,
|
||||||
|
lockedTooltip = "(Cannot change while currently syncing. Disconnect first.)",
|
||||||
tag = "unstable",
|
tag = "unstable",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = layoutIncrement(),
|
layoutOrder = layoutIncrement(),
|
||||||
@@ -259,7 +287,6 @@ function SettingsPage:render()
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return SettingsPage
|
return SettingsPage
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
--[[
|
--[[
|
||||||
Theming system taking advantage of Roact's new context API.
|
Theming system provided through Roact's context.
|
||||||
Doesn't use colors provided by Studio and instead just branches on theme
|
Uses Studio colors when possible.
|
||||||
name. This isn't exactly best practice.
|
|
||||||
]]
|
]]
|
||||||
|
|
||||||
-- Studio does not exist outside Roblox Studio, so we'll lazily initialize it
|
-- Studio does not exist outside Roblox Studio, so we'll lazily initialize it
|
||||||
@@ -15,6 +14,8 @@ local function getStudio()
|
|||||||
return _Studio
|
return _Studio
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local ContentProvider = game:GetService("ContentProvider")
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
@@ -35,6 +36,27 @@ function StudioProvider:updateTheme()
|
|||||||
local isDark = studioTheme.Name == "Dark"
|
local isDark = studioTheme.Name == "Dark"
|
||||||
|
|
||||||
local theme = strict(studioTheme.Name .. "Theme", {
|
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,
|
BrandColor = BRAND_COLOR,
|
||||||
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
|
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
|
||||||
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
|
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
|
||||||
@@ -143,12 +165,29 @@ function StudioProvider:updateTheme()
|
|||||||
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
|
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
|
||||||
},
|
},
|
||||||
Diff = {
|
Diff = {
|
||||||
|
-- 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
|
-- 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),
|
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),
|
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),
|
Edit = if isDark then Color3.fromRGB(120, 154, 248) else Color3.fromRGB(0, 70, 160),
|
||||||
Row = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
Remain = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
Warning = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText),
|
},
|
||||||
|
|
||||||
|
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 = {
|
ConnectionDetails = {
|
||||||
ProjectNameColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
ProjectNameColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
@@ -190,6 +229,13 @@ end
|
|||||||
|
|
||||||
function StudioProvider:init()
|
function StudioProvider:init()
|
||||||
self:updateTheme()
|
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
|
end
|
||||||
|
|
||||||
function StudioProvider:render()
|
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 Roact = require(Packages.Roact)
|
||||||
local Log = require(Packages.Log)
|
local Log = require(Packages.Log)
|
||||||
|
local Promise = require(Packages.Promise)
|
||||||
|
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local Version = require(Plugin.Version)
|
local Version = require(Plugin.Version)
|
||||||
@@ -27,7 +28,7 @@ local timeUtil = require(Plugin.timeUtil)
|
|||||||
local Theme = require(script.Theme)
|
local Theme = require(script.Theme)
|
||||||
|
|
||||||
local Page = require(script.Page)
|
local Page = require(script.Page)
|
||||||
local Notifications = require(script.Notifications)
|
local Notifications = require(script.Components.Notifications)
|
||||||
local Tooltip = require(script.Components.Tooltip)
|
local Tooltip = require(script.Components.Tooltip)
|
||||||
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
|
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
|
||||||
local StudioToolbar = require(script.Components.Studio.StudioToolbar)
|
local StudioToolbar = require(script.Components.Studio.StudioToolbar)
|
||||||
@@ -52,9 +53,9 @@ local App = Roact.Component:extend("App")
|
|||||||
function App:init()
|
function App:init()
|
||||||
preloadAssets()
|
preloadAssets()
|
||||||
|
|
||||||
local priorHost, priorPort = self:getPriorEndpoint()
|
local priorSyncInfo = self:getPriorSyncInfo()
|
||||||
self.host, self.setHost = Roact.createBinding(priorHost or "")
|
self.host, self.setHost = Roact.createBinding(priorSyncInfo.host or "")
|
||||||
self.port, self.setPort = Roact.createBinding(priorPort or "")
|
self.port, self.setPort = Roact.createBinding(priorSyncInfo.port or "")
|
||||||
|
|
||||||
self.confirmationBindable = Instance.new("BindableEvent")
|
self.confirmationBindable = Instance.new("BindableEvent")
|
||||||
self.confirmationEvent = self.confirmationBindable.Event
|
self.confirmationEvent = self.confirmationBindable.Event
|
||||||
@@ -78,17 +79,18 @@ function App:init()
|
|||||||
action
|
action
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
local dismissNotif = self:addNotification(
|
local dismissNotif = self:addNotification({
|
||||||
string.format("You've undone '%s'.\nIf this was not intended, please restore.", action),
|
text = string.format("You've undone '%s'.\nIf this was not intended, please restore.", action),
|
||||||
10,
|
timeout = 10,
|
||||||
{
|
onClose = function()
|
||||||
|
cleanup()
|
||||||
|
end,
|
||||||
|
actions = {
|
||||||
Restore = {
|
Restore = {
|
||||||
text = "Restore",
|
text = "Restore",
|
||||||
style = "Solid",
|
style = "Solid",
|
||||||
layoutOrder = 1,
|
layoutOrder = 1,
|
||||||
onClick = function(notification)
|
onClick = function()
|
||||||
cleanup()
|
|
||||||
notification:dismiss()
|
|
||||||
ChangeHistoryService:Redo()
|
ChangeHistoryService:Redo()
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
@@ -96,13 +98,9 @@ function App:init()
|
|||||||
text = "Dismiss",
|
text = "Dismiss",
|
||||||
style = "Bordered",
|
style = "Bordered",
|
||||||
layoutOrder = 2,
|
layoutOrder = 2,
|
||||||
onClick = function(notification)
|
|
||||||
cleanup()
|
|
||||||
notification:dismiss()
|
|
||||||
end,
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
})
|
||||||
|
|
||||||
undoConnection = ChangeHistoryService.OnUndo:Once(function()
|
undoConnection = ChangeHistoryService.OnUndo:Once(function()
|
||||||
-- Our notif is now out of date- redoing will not restore the patch
|
-- Our notif is now out of date- redoing will not restore the patch
|
||||||
@@ -142,32 +140,20 @@ function App:init()
|
|||||||
if RunService:IsEdit() then
|
if RunService:IsEdit() then
|
||||||
self:checkForUpdates()
|
self:checkForUpdates()
|
||||||
|
|
||||||
if
|
self:startSyncReminderPolling()
|
||||||
Settings:get("syncReminder")
|
self.disconnectSyncReminderPollingChanged = Settings:onChanged("syncReminderPolling", function(enabled)
|
||||||
and self.serveSession == nil
|
if enabled then
|
||||||
and self:getLastSyncTimestamp()
|
self:startSyncReminderPolling()
|
||||||
and (self:isSyncLockAvailable())
|
else
|
||||||
then
|
self:stopSyncReminderPolling()
|
||||||
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
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
self:tryAutoReconnect():andThen(function(didReconnect)
|
||||||
|
if not didReconnect then
|
||||||
|
self:checkSyncReminder()
|
||||||
|
end
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
if self:isAutoConnectPlaytestServerAvailable() then
|
if self:isAutoConnectPlaytestServerAvailable() then
|
||||||
@@ -193,16 +179,23 @@ function App:willUnmount()
|
|||||||
|
|
||||||
self.disconnectUpdatesCheckChanged()
|
self.disconnectUpdatesCheckChanged()
|
||||||
self.disconnectPrereleasesCheckChanged()
|
self.disconnectPrereleasesCheckChanged()
|
||||||
|
if self.disconnectSyncReminderPollingChanged then
|
||||||
|
self.disconnectSyncReminderPollingChanged()
|
||||||
|
end
|
||||||
|
|
||||||
|
self:stopSyncReminderPolling()
|
||||||
|
|
||||||
self.autoConnectPlaytestServerListener()
|
self.autoConnectPlaytestServerListener()
|
||||||
self:clearRunningConnectionInfo()
|
self:clearRunningConnectionInfo()
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:addNotification(
|
function App:addNotification(notif: {
|
||||||
text: string,
|
text: string,
|
||||||
|
isFullscreen: boolean?,
|
||||||
timeout: number?,
|
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
|
if not Settings:get("showNotifications") then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -210,17 +203,17 @@ function App:addNotification(
|
|||||||
self.notifId += 1
|
self.notifId += 1
|
||||||
local id = self.notifId
|
local id = self.notifId
|
||||||
|
|
||||||
local notifications = table.clone(self.state.notifications)
|
self:setState(function(prevState)
|
||||||
notifications[id] = {
|
local notifications = table.clone(prevState.notifications)
|
||||||
text = text,
|
notifications[id] = Dictionary.merge({
|
||||||
timestamp = DateTime.now().UnixTimestampMillis,
|
timeout = notif.timeout or 5,
|
||||||
timeout = timeout or 3,
|
isFullscreen = notif.isFullscreen or false,
|
||||||
actions = actions,
|
}, notif)
|
||||||
}
|
|
||||||
|
|
||||||
self:setState({
|
return {
|
||||||
notifications = notifications,
|
notifications = notifications,
|
||||||
})
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
self:closeNotification(id)
|
self:closeNotification(id)
|
||||||
@@ -232,96 +225,60 @@ function App:closeNotification(id: number)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local notifications = table.clone(self.state.notifications)
|
self:setState(function(prevState)
|
||||||
|
local notifications = table.clone(prevState.notifications)
|
||||||
notifications[id] = nil
|
notifications[id] = nil
|
||||||
|
|
||||||
self:setState({
|
return {
|
||||||
notifications = notifications,
|
notifications = notifications,
|
||||||
})
|
}
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:checkForUpdates()
|
function App:checkForUpdates()
|
||||||
if not Settings:get("checkForUpdates") then
|
local updateMessage = Version.getUpdateMessage()
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local isLocalInstall = string.find(debug.traceback(), "\n[^\n]-user_.-$") ~= nil
|
if updateMessage then
|
||||||
local latestCompatibleVersion = Version.retrieveLatestCompatible({
|
self:addNotification({
|
||||||
version = Config.version,
|
text = updateMessage,
|
||||||
includePrereleases = isLocalInstall and Settings:get("checkForPrereleases"),
|
timeout = 500,
|
||||||
})
|
actions = {
|
||||||
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 = {
|
Dismiss = {
|
||||||
text = "Dismiss",
|
text = "Dismiss",
|
||||||
style = "Bordered",
|
style = "Bordered",
|
||||||
layoutOrder = 2,
|
layoutOrder = 2,
|
||||||
onClick = function(notification)
|
|
||||||
notification:dismiss()
|
|
||||||
end,
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:getPriorEndpoint()
|
function App:getPriorSyncInfo(): { host: string?, port: string?, projectName: string?, timestamp: number? }
|
||||||
local priorEndpoints = Settings:get("priorEndpoints")
|
local priorSyncInfos = Settings:get("priorEndpoints")
|
||||||
if not priorEndpoints then
|
if not priorSyncInfos then
|
||||||
return
|
return {}
|
||||||
end
|
end
|
||||||
|
|
||||||
local id = tostring(game.PlaceId)
|
local id = tostring(game.PlaceId)
|
||||||
if ignorePlaceIds[id] then
|
if ignorePlaceIds[id] then
|
||||||
return
|
return {}
|
||||||
end
|
end
|
||||||
|
|
||||||
local place = priorEndpoints[id]
|
return priorSyncInfos[id] or {}
|
||||||
if not place then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
return place.host, place.port
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:getLastSyncTimestamp()
|
function App:setPriorSyncInfo(host: string, port: string, projectName: string)
|
||||||
local priorEndpoints = Settings:get("priorEndpoints")
|
local priorSyncInfos = Settings:get("priorEndpoints")
|
||||||
if not priorEndpoints then
|
if not priorSyncInfos then
|
||||||
return
|
priorSyncInfos = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
local id = tostring(game.PlaceId)
|
local now = os.time()
|
||||||
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
|
|
||||||
|
|
||||||
-- Clear any stale saves to avoid disc bloat
|
-- Clear any stale saves to avoid disc bloat
|
||||||
for placeId, endpoint in priorEndpoints do
|
for placeId, syncInfo in priorSyncInfos do
|
||||||
if os.time() - endpoint.timestamp > 12_960_000 then
|
if now - (syncInfo.timestamp or now) > 12_960_000 then
|
||||||
priorEndpoints[placeId] = nil
|
priorSyncInfos[placeId] = nil
|
||||||
Log.trace("Cleared stale saved endpoint for {}", placeId)
|
Log.trace("Cleared stale saved endpoint for {}", placeId)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -331,14 +288,15 @@ function App:setPriorEndpoint(host: string, port: string)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
priorEndpoints[id] = {
|
priorSyncInfos[id] = {
|
||||||
host = if host ~= Config.defaultHost then host else nil,
|
host = if host ~= Config.defaultHost then host else nil,
|
||||||
port = if port ~= Config.defaultPort then port 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)
|
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
||||||
|
|
||||||
Settings:set("priorEndpoints", priorEndpoints)
|
Settings:set("priorEndpoints", priorSyncInfos)
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:getHostAndPort()
|
function App:getHostAndPort()
|
||||||
@@ -413,8 +371,158 @@ function App:releaseSyncLock()
|
|||||||
Log.trace("Could not relase sync lock because it is owned by {}", lock.Value)
|
Log.trace("Could not relase sync lock because it is owned by {}", lock.Value)
|
||||||
end
|
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()
|
function App:isAutoConnectPlaytestServerAvailable()
|
||||||
return RunService:IsRunMode()
|
return RunService:IsRunning()
|
||||||
|
and RunService:IsStudio()
|
||||||
and RunService:IsServer()
|
and RunService:IsServer()
|
||||||
and Settings:get("autoConnectPlaytestServer")
|
and Settings:get("autoConnectPlaytestServer")
|
||||||
and workspace:GetAttribute("__Rojo_ConnectionUrl")
|
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))
|
local msg = string.format("Could not sync because user '%s' is already syncing", tostring(priorOwner))
|
||||||
|
|
||||||
Log.warn(msg)
|
Log.warn(msg)
|
||||||
self:addNotification(msg, 10)
|
self:addNotification({
|
||||||
|
text = msg,
|
||||||
|
timeout = 10,
|
||||||
|
})
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.Error,
|
appStatus = AppStatus.Error,
|
||||||
errorMessage = msg,
|
errorMessage = msg,
|
||||||
@@ -484,13 +595,13 @@ function App:startSession()
|
|||||||
twoWaySync = Settings:get("twoWaySync"),
|
twoWaySync = Settings:get("twoWaySync"),
|
||||||
})
|
})
|
||||||
|
|
||||||
self.cleanupPrecommit = serveSession.__reconciler:hookPrecommit(function(patch, instanceMap)
|
self.cleanupPrecommit = serveSession:hookPrecommit(function(patch, instanceMap)
|
||||||
-- Build new tree for patch
|
-- Build new tree for patch
|
||||||
self:setState({
|
self:setState({
|
||||||
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Old", "New" }),
|
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Old", "New" }),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
self.cleanupPostcommit = serveSession.__reconciler:hookPostcommit(function(patch, instanceMap, unappliedPatch)
|
self.cleanupPostcommit = serveSession:hookPostcommit(function(patch, instanceMap, unappliedPatch)
|
||||||
-- Update tree with unapplied metadata
|
-- Update tree with unapplied metadata
|
||||||
self:setState(function(prevState)
|
self:setState(function(prevState)
|
||||||
return {
|
return {
|
||||||
@@ -533,15 +644,21 @@ function App:startSession()
|
|||||||
|
|
||||||
serveSession:onStatusChanged(function(status, details)
|
serveSession:onStatusChanged(function(status, details)
|
||||||
if status == ServeSession.Status.Connecting then
|
if status == ServeSession.Status.Connecting then
|
||||||
self:setPriorEndpoint(host, port)
|
if self.dismissSyncReminder then
|
||||||
|
self.dismissSyncReminder()
|
||||||
|
self.dismissSyncReminder = nil
|
||||||
|
end
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.Connecting,
|
appStatus = AppStatus.Connecting,
|
||||||
toolbarIcon = Assets.Images.PluginButton,
|
toolbarIcon = Assets.Images.PluginButton,
|
||||||
})
|
})
|
||||||
self:addNotification("Connecting to session...")
|
self:addNotification({
|
||||||
|
text = "Connecting to session...",
|
||||||
|
})
|
||||||
elseif status == ServeSession.Status.Connected then
|
elseif status == ServeSession.Status.Connected then
|
||||||
self.knownProjects[details] = true
|
self.knownProjects[details] = true
|
||||||
|
self:setPriorSyncInfo(host, port, details)
|
||||||
self:setRunningConnectionInfo(baseUrl)
|
self:setRunningConnectionInfo(baseUrl)
|
||||||
|
|
||||||
local address = ("%s:%s"):format(host, port)
|
local address = ("%s:%s"):format(host, port)
|
||||||
@@ -551,7 +668,9 @@ function App:startSession()
|
|||||||
address = address,
|
address = address,
|
||||||
toolbarIcon = Assets.Images.PluginButtonConnected,
|
toolbarIcon = Assets.Images.PluginButtonConnected,
|
||||||
})
|
})
|
||||||
self:addNotification(string.format("Connected to session '%s' at %s.", details, address), 5)
|
self:addNotification({
|
||||||
|
text = string.format("Connected to session '%s' at %s.", details, address),
|
||||||
|
})
|
||||||
elseif status == ServeSession.Status.Disconnected then
|
elseif status == ServeSession.Status.Disconnected then
|
||||||
self.serveSession = nil
|
self.serveSession = nil
|
||||||
self:releaseSyncLock()
|
self:releaseSyncLock()
|
||||||
@@ -574,13 +693,19 @@ function App:startSession()
|
|||||||
errorMessage = tostring(details),
|
errorMessage = tostring(details),
|
||||||
toolbarIcon = Assets.Images.PluginButtonWarning,
|
toolbarIcon = Assets.Images.PluginButtonWarning,
|
||||||
})
|
})
|
||||||
self:addNotification(tostring(details), 10)
|
self:addNotification({
|
||||||
|
text = tostring(details),
|
||||||
|
timeout = 10,
|
||||||
|
})
|
||||||
else
|
else
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.NotConnected,
|
appStatus = AppStatus.NotConnected,
|
||||||
toolbarIcon = Assets.Images.PluginButton,
|
toolbarIcon = Assets.Images.PluginButton,
|
||||||
})
|
})
|
||||||
self:addNotification("Disconnected from session.")
|
self:addNotification({
|
||||||
|
text = "Disconnected from session.",
|
||||||
|
timeout = 10,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
@@ -658,13 +783,13 @@ function App:startSession()
|
|||||||
toolbarIcon = Assets.Images.PluginButton,
|
toolbarIcon = Assets.Images.PluginButton,
|
||||||
})
|
})
|
||||||
|
|
||||||
self:addNotification(
|
self:addNotification({
|
||||||
string.format(
|
text = string.format(
|
||||||
"Please accept%sor abort the initializing sync session.",
|
"Please accept%sor abort the initializing sync session.",
|
||||||
Settings:get("twoWaySync") and ", reject, " or " "
|
Settings:get("twoWaySync") and ", reject, " or " "
|
||||||
),
|
),
|
||||||
7
|
timeout = 7,
|
||||||
)
|
})
|
||||||
|
|
||||||
return self.confirmationEvent:Wait()
|
return self.confirmationEvent:Wait()
|
||||||
end)
|
end)
|
||||||
@@ -825,19 +950,7 @@ function App:render()
|
|||||||
ResetOnSpawn = false,
|
ResetOnSpawn = false,
|
||||||
DisplayOrder = 100,
|
DisplayOrder = 100,
|
||||||
}, {
|
}, {
|
||||||
layout = e("UIListLayout", {
|
Notifications = e(Notifications, {
|
||||||
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, {
|
|
||||||
soundPlayer = self.props.soundPlayer,
|
soundPlayer = self.props.soundPlayer,
|
||||||
notifications = self.state.notifications,
|
notifications = self.state.notifications,
|
||||||
onClose = function(id)
|
onClose = function(id)
|
||||||
|
|||||||
@@ -282,6 +282,22 @@ function PatchSet.assign(target, ...)
|
|||||||
return target
|
return target
|
||||||
end
|
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
|
Create a list of human-readable statements summarizing the contents of this
|
||||||
patch, intended to be displayed to users.
|
patch, intended to be displayed to users.
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
Patches can come from the server or be generated by the client.
|
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 Packages = script.Parent.Parent.Parent.Packages
|
||||||
local Log = require(Packages.Log)
|
local Log = require(Packages.Log)
|
||||||
|
|
||||||
@@ -16,19 +14,18 @@ local invariant = require(script.Parent.Parent.invariant)
|
|||||||
|
|
||||||
local decodeValue = require(script.Parent.decodeValue)
|
local decodeValue = require(script.Parent.decodeValue)
|
||||||
local reify = require(script.Parent.reify)
|
local reify = require(script.Parent.reify)
|
||||||
|
local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferredRefs
|
||||||
local setProperty = require(script.Parent.setProperty)
|
local setProperty = require(script.Parent.setProperty)
|
||||||
|
|
||||||
local function applyPatch(instanceMap, patch)
|
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.
|
-- Tracks any portions of the patch that could not be applied to the DOM.
|
||||||
local unappliedPatch = PatchSet.newEmpty()
|
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
|
for _, removedIdOrInstance in ipairs(patch.removed) do
|
||||||
local removeInstanceSuccess = pcall(function()
|
local removeInstanceSuccess = pcall(function()
|
||||||
if Types.RbxId(removedIdOrInstance) then
|
if Types.RbxId(removedIdOrInstance) then
|
||||||
@@ -67,9 +64,6 @@ local function applyPatch(instanceMap, patch)
|
|||||||
if parentInstance == nil then
|
if parentInstance == nil then
|
||||||
-- This would be peculiar. If you create an instance with no
|
-- This would be peculiar. If you create an instance with no
|
||||||
-- parent, were you supposed to create it at all?
|
-- parent, were you supposed to create it at all?
|
||||||
if historyRecording then
|
|
||||||
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
|
|
||||||
end
|
|
||||||
invariant(
|
invariant(
|
||||||
"Cannot add an instance from a patch that has no parent.\nInstance {} with parent {}.\nState: {:#?}",
|
"Cannot add an instance from a patch that has no parent.\nInstance {} with parent {}.\nState: {:#?}",
|
||||||
id,
|
id,
|
||||||
@@ -78,7 +72,7 @@ local function applyPatch(instanceMap, patch)
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
local failedToReify = reify(instanceMap, patch.added, id, parentInstance)
|
local failedToReify = reifyInstance(deferredRefs, instanceMap, patch.added, id, parentInstance)
|
||||||
|
|
||||||
if not PatchSet.isEmpty(failedToReify) then
|
if not PatchSet.isEmpty(failedToReify) then
|
||||||
Log.debug("Failed to reify as part of applying a patch: {:#?}", failedToReify)
|
Log.debug("Failed to reify as part of applying a patch: {:#?}", failedToReify)
|
||||||
@@ -143,7 +137,7 @@ local function applyPatch(instanceMap, patch)
|
|||||||
[update.id] = mockVirtualInstance,
|
[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]
|
local newInstance = instanceMap.fromIds[update.id]
|
||||||
|
|
||||||
@@ -206,6 +200,18 @@ local function applyPatch(instanceMap, patch)
|
|||||||
|
|
||||||
if update.changedProperties ~= nil then
|
if update.changedProperties ~= nil then
|
||||||
for propertyName, propertyValue in pairs(update.changedProperties) do
|
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)
|
local decodeSuccess, decodedValue = decodeValue(propertyValue, instanceMap)
|
||||||
if not decodeSuccess then
|
if not decodeSuccess then
|
||||||
unappliedUpdate.changedProperties[propertyName] = propertyValue
|
unappliedUpdate.changedProperties[propertyName] = propertyValue
|
||||||
@@ -226,9 +232,7 @@ local function applyPatch(instanceMap, patch)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if historyRecording then
|
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||||
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
|
|
||||||
end
|
|
||||||
|
|
||||||
return unappliedPatch
|
return unappliedPatch
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ local function trueEquals(a, b): boolean
|
|||||||
return true
|
return true
|
||||||
end
|
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)
|
local typeA, typeB = typeof(a), typeof(b)
|
||||||
|
|
||||||
-- For tables, try recursive deep equality
|
-- For tables, try recursive deep equality
|
||||||
@@ -151,7 +159,24 @@ local function diff(instanceMap, virtualInstances, rootId)
|
|||||||
|
|
||||||
if getProperySuccess then
|
if getProperySuccess then
|
||||||
local existingValue = existingValueOrErr
|
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 decodeSuccess then
|
||||||
if not trueEquals(existingValue, decodedValue) then
|
if not trueEquals(existingValue, decodedValue) then
|
||||||
|
|||||||
@@ -5,9 +5,6 @@
|
|||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
|
||||||
|
|
||||||
local Log = require(Packages.Log)
|
|
||||||
|
|
||||||
local Timer = require(Plugin.Timer)
|
local Timer = require(Plugin.Timer)
|
||||||
|
|
||||||
@@ -22,78 +19,17 @@ function Reconciler.new(instanceMap)
|
|||||||
local self = {
|
local self = {
|
||||||
-- Tracks all of the instances known by the reconciler by ID.
|
-- Tracks all of the instances known by the reconciler by ID.
|
||||||
__instanceMap = instanceMap,
|
__instanceMap = instanceMap,
|
||||||
__precommitCallbacks = {},
|
|
||||||
__postcommitCallbacks = {},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return setmetatable(self, Reconciler)
|
return setmetatable(self, Reconciler)
|
||||||
end
|
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)
|
function Reconciler:applyPatch(patch)
|
||||||
Timer.start("Reconciler:applyPatch")
|
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)
|
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()
|
Timer.stop()
|
||||||
|
|
||||||
return unappliedPatch
|
return unappliedPatch
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,26 +7,6 @@ local PatchSet = require(script.Parent.Parent.PatchSet)
|
|||||||
local setProperty = require(script.Parent.setProperty)
|
local setProperty = require(script.Parent.setProperty)
|
||||||
local decodeValue = require(script.Parent.decodeValue)
|
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
|
Add the given ID and all of its descendants in virtualInstances to the given
|
||||||
PatchSet, marked for addition.
|
PatchSet, marked for addition.
|
||||||
@@ -40,10 +20,21 @@ local function addAllToPatch(patchSet, virtualInstances, id)
|
|||||||
end
|
end
|
||||||
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.
|
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]
|
local virtualInstance = virtualInstances[id]
|
||||||
|
|
||||||
if virtualInstance == nil then
|
if virtualInstance == nil then
|
||||||
@@ -102,7 +93,7 @@ function reifyInner(instanceMap, virtualInstances, id, parentInstance, unapplied
|
|||||||
end
|
end
|
||||||
|
|
||||||
for _, childId in ipairs(virtualInstance.Children) do
|
for _, childId in ipairs(virtualInstance.Children) do
|
||||||
reifyInner(instanceMap, virtualInstances, childId, instance, unappliedPatch, deferredRefs)
|
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, childId, instance)
|
||||||
end
|
end
|
||||||
|
|
||||||
instance.Parent = parentInstance
|
instance.Parent = parentInstance
|
||||||
@@ -143,6 +134,7 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local targetInstance = instanceMap.fromIds[refId]
|
local targetInstance = instanceMap.fromIds[refId]
|
||||||
|
|
||||||
if targetInstance == nil then
|
if targetInstance == nil then
|
||||||
markFailed(entry.id, entry.propertyName, entry.virtualValue)
|
markFailed(entry.id, entry.propertyName, entry.virtualValue)
|
||||||
continue
|
continue
|
||||||
@@ -155,4 +147,7 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return reify
|
return {
|
||||||
|
reifyInstance = reifyInstance,
|
||||||
|
applyDeferredRefs = applyDeferredRefs,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
return function()
|
return function()
|
||||||
local reify = require(script.Parent.reify)
|
local reify = require(script.Parent.reify)
|
||||||
|
local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferredRefs
|
||||||
|
|
||||||
local PatchSet = require(script.Parent.Parent.PatchSet)
|
local PatchSet = require(script.Parent.Parent.PatchSet)
|
||||||
local InstanceMap = require(script.Parent.Parent.InstanceMap)
|
local InstanceMap = require(script.Parent.Parent.InstanceMap)
|
||||||
@@ -20,7 +21,11 @@ return function()
|
|||||||
|
|
||||||
it("should throw when given a bogus ID", function()
|
it("should throw when given a bogus ID", function()
|
||||||
expect(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).to.throw()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -34,8 +39,11 @@ return function()
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local deferredRefs = {}
|
||||||
local instanceMap = InstanceMap.new()
|
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")
|
assert(instanceMap:size() == 0, "expected instanceMap to be empty")
|
||||||
|
|
||||||
@@ -60,8 +68,11 @@ return function()
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local deferredRefs = {}
|
||||||
local instanceMap = InstanceMap.new()
|
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")
|
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||||
|
|
||||||
@@ -90,8 +101,11 @@ return function()
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local deferredRefs = {}
|
||||||
local instanceMap = InstanceMap.new()
|
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")
|
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||||
|
|
||||||
@@ -122,8 +136,11 @@ return function()
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local deferredRefs = {}
|
||||||
local instanceMap = InstanceMap.new()
|
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(size(unappliedPatch.added)).to.equal(1)
|
||||||
expect(unappliedPatch.added["CHILD"]).to.equal(virtualInstances["CHILD"])
|
expect(unappliedPatch.added["CHILD"]).to.equal(virtualInstances["CHILD"])
|
||||||
@@ -153,8 +170,11 @@ return function()
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local deferredRefs = {}
|
||||||
local instanceMap = InstanceMap.new()
|
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"]
|
local instance = instanceMap.fromIds["ROOT"]
|
||||||
expect(instance.ClassName).to.equal("StringValue")
|
expect(instance.ClassName).to.equal("StringValue")
|
||||||
@@ -196,8 +216,11 @@ return function()
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local deferredRefs = {}
|
||||||
local instanceMap = InstanceMap.new()
|
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")
|
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||||
|
|
||||||
@@ -223,13 +246,16 @@ return function()
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local deferredRefs = {}
|
||||||
local instanceMap = InstanceMap.new()
|
local instanceMap = InstanceMap.new()
|
||||||
|
|
||||||
local existing = Instance.new("Folder")
|
local existing = Instance.new("Folder")
|
||||||
existing.Name = "Existing"
|
existing.Name = "Existing"
|
||||||
instanceMap:insert("EXISTING", 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")
|
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||||
|
|
||||||
@@ -268,8 +294,11 @@ return function()
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local deferredRefs = {}
|
||||||
local instanceMap = InstanceMap.new()
|
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")
|
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||||
|
|
||||||
@@ -307,8 +336,11 @@ return function()
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local deferredRefs = {}
|
||||||
local instanceMap = InstanceMap.new()
|
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")
|
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||||
|
|
||||||
@@ -332,8 +364,11 @@ return function()
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local deferredRefs = {}
|
||||||
local instanceMap = InstanceMap.new()
|
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.hasRemoves(unappliedPatch), "expected no removes")
|
||||||
assert(not PatchSet.hasAdditions(unappliedPatch), "expected no additions")
|
assert(not PatchSet.hasAdditions(unappliedPatch), "expected no additions")
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
local StudioService = game:GetService("StudioService")
|
local StudioService = game:GetService("StudioService")
|
||||||
local RunService = game:GetService("RunService")
|
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 Packages = script.Parent.Parent.Packages
|
||||||
local Log = require(Packages.Log)
|
local Log = require(Packages.Log)
|
||||||
local Fmt = require(Packages.Fmt)
|
local Fmt = require(Packages.Fmt)
|
||||||
local t = require(Packages.t)
|
local t = require(Packages.t)
|
||||||
local Promise = require(Packages.Promise)
|
local Promise = require(Packages.Promise)
|
||||||
|
local Timer = require(script.Parent.Timer)
|
||||||
|
|
||||||
local ChangeBatcher = require(script.Parent.ChangeBatcher)
|
local ChangeBatcher = require(script.Parent.ChangeBatcher)
|
||||||
local encodePatchUpdate = require(script.Parent.ChangeBatcher.encodePatchUpdate)
|
local encodePatchUpdate = require(script.Parent.ChangeBatcher.encodePatchUpdate)
|
||||||
@@ -95,6 +99,8 @@ function ServeSession.new(options)
|
|||||||
__changeBatcher = changeBatcher,
|
__changeBatcher = changeBatcher,
|
||||||
__statusChangedCallback = nil,
|
__statusChangedCallback = nil,
|
||||||
__connections = connections,
|
__connections = connections,
|
||||||
|
__precommitCallbacks = {},
|
||||||
|
__postcommitCallbacks = {},
|
||||||
}
|
}
|
||||||
|
|
||||||
setmetatable(self, ServeSession)
|
setmetatable(self, ServeSession)
|
||||||
@@ -125,12 +131,46 @@ function ServeSession:setConfirmCallback(callback)
|
|||||||
self.__userConfirmCallback = callback
|
self.__userConfirmCallback = callback
|
||||||
end
|
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)
|
function ServeSession:hookPrecommit(callback)
|
||||||
return self.__reconciler: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
|
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)
|
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
|
end
|
||||||
|
|
||||||
function ServeSession:start()
|
function ServeSession:start()
|
||||||
@@ -139,10 +179,9 @@ function ServeSession:start()
|
|||||||
self.__apiContext
|
self.__apiContext
|
||||||
:connect()
|
:connect()
|
||||||
:andThen(function(serverInfo)
|
:andThen(function(serverInfo)
|
||||||
self:__applyGameAndPlaceId(serverInfo)
|
|
||||||
|
|
||||||
return self:__initialSync(serverInfo):andThen(function()
|
return self:__initialSync(serverInfo):andThen(function()
|
||||||
self:__setStatus(Status.Connected, serverInfo.projectName)
|
self:__setStatus(Status.Connected, serverInfo.projectName)
|
||||||
|
self:__applyGameAndPlaceId(serverInfo)
|
||||||
|
|
||||||
return self:__mainSyncLoop()
|
return self:__mainSyncLoop()
|
||||||
end)
|
end)
|
||||||
@@ -207,6 +246,169 @@ function ServeSession:__onActiveScriptChanged(activeScript)
|
|||||||
self.__apiContext:open(scriptId)
|
self.__apiContext:open(scriptId)
|
||||||
end
|
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]
|
||||||
|
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
|
||||||
|
child.Parent = replacement
|
||||||
|
end
|
||||||
|
|
||||||
|
replacement.Parent = oldParent
|
||||||
|
-- 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`.
|
||||||
|
oldInstance.Parent = nil
|
||||||
|
|
||||||
|
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 PatchSet.isEmpty(unappliedPatch) then
|
||||||
|
if historyRecording then
|
||||||
|
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local addedIdList = PatchSet.addedIdList(unappliedPatch)
|
||||||
|
local updatedIdList = PatchSet.updatedIdList(unappliedPatch)
|
||||||
|
|
||||||
|
local actualUnappliedPatches = PatchSet.newEmpty()
|
||||||
|
if Settings:get("enableSyncFallback") then
|
||||||
|
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()
|
||||||
|
|
||||||
|
if addSuccess then
|
||||||
|
table.clear(unappliedPatch.added)
|
||||||
|
PatchSet.assign(actualUnappliedPatches, unappliedAddedRefs)
|
||||||
|
end
|
||||||
|
if updateSuccess then
|
||||||
|
table.clear(unappliedPatch.updated)
|
||||||
|
PatchSet.assign(actualUnappliedPatches, unappliedUpdateRefs)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Log.debug("Skipping ServeSession:__replaceInstances because of setting")
|
||||||
|
end
|
||||||
|
PatchSet.assign(actualUnappliedPatches, unappliedPatch)
|
||||||
|
|
||||||
|
if not PatchSet.isEmpty(actualUnappliedPatches) 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, actualUnappliedPatches)
|
||||||
|
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)
|
function ServeSession:__initialSync(serverInfo)
|
||||||
return self.__apiContext:read({ serverInfo.rootInstanceId }):andThen(function(readResponseBody)
|
return self.__apiContext:read({ serverInfo.rootInstanceId }):andThen(function(readResponseBody)
|
||||||
-- Tell the API Context that we're up-to-date with the version of
|
-- Tell the API Context that we're up-to-date with the version of
|
||||||
@@ -281,15 +483,7 @@ function ServeSession:__initialSync(serverInfo)
|
|||||||
|
|
||||||
return self.__apiContext:write(inversePatch)
|
return self.__apiContext:write(inversePatch)
|
||||||
elseif userDecision == "Accept" then
|
elseif userDecision == "Accept" then
|
||||||
local unappliedPatch = self.__reconciler:applyPatch(catchUpPatch)
|
self:__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
|
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
else
|
else
|
||||||
return Promise.reject("Invalid user decision: " .. userDecision)
|
return Promise.reject("Invalid user decision: " .. userDecision)
|
||||||
@@ -312,14 +506,7 @@ function ServeSession:__mainSyncLoop()
|
|||||||
Log.trace("Serve session {} retrieved {} messages", tostring(self), #messages)
|
Log.trace("Serve session {} retrieved {} messages", tostring(self), #messages)
|
||||||
|
|
||||||
for _, message in messages do
|
for _, message in messages do
|
||||||
local unappliedPatch = self.__reconciler:applyPatch(message)
|
self:__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
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
:await()
|
:await()
|
||||||
|
|||||||
@@ -12,12 +12,15 @@ local Roact = require(Packages.Roact)
|
|||||||
local defaultSettings = {
|
local defaultSettings = {
|
||||||
openScriptsExternally = false,
|
openScriptsExternally = false,
|
||||||
twoWaySync = false,
|
twoWaySync = false,
|
||||||
|
autoReconnect = false,
|
||||||
showNotifications = true,
|
showNotifications = true,
|
||||||
syncReminder = true,
|
enableSyncFallback = true,
|
||||||
|
syncReminderMode = "Notify" :: "None" | "Notify" | "Fullscreen",
|
||||||
|
syncReminderPolling = true,
|
||||||
checkForUpdates = true,
|
checkForUpdates = true,
|
||||||
checkForPrereleases = false,
|
checkForPrereleases = false,
|
||||||
autoConnectPlaytestServer = false,
|
autoConnectPlaytestServer = false,
|
||||||
confirmationBehavior = "Initial",
|
confirmationBehavior = "Initial" :: "Never" | "Initial" | "Large Changes" | "Unlisted PlaceId",
|
||||||
largeChangesConfirmationThreshold = 5,
|
largeChangesConfirmationThreshold = 5,
|
||||||
playSounds = true,
|
playSounds = true,
|
||||||
typecheckingEnabled = false,
|
typecheckingEnabled = false,
|
||||||
@@ -108,4 +111,14 @@ function Settings:getBinding(name)
|
|||||||
return bind
|
return bind
|
||||||
end
|
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
|
return Settings
|
||||||
|
|||||||
@@ -55,6 +55,16 @@ local ApiSubscribeResponse = t.interface({
|
|||||||
messages = t.array(ApiSubscribeMessage),
|
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({
|
local ApiError = t.interface({
|
||||||
kind = t.union(t.literal("NotFound"), t.literal("BadRequest"), t.literal("InternalError")),
|
kind = t.union(t.literal("NotFound"), t.literal("BadRequest"), t.literal("InternalError")),
|
||||||
details = t.string,
|
details = t.string,
|
||||||
@@ -82,6 +92,8 @@ return strict("Types", {
|
|||||||
ApiInstanceUpdate = ApiInstanceUpdate,
|
ApiInstanceUpdate = ApiInstanceUpdate,
|
||||||
ApiInstanceMetadata = ApiInstanceMetadata,
|
ApiInstanceMetadata = ApiInstanceMetadata,
|
||||||
ApiSubscribeMessage = ApiSubscribeMessage,
|
ApiSubscribeMessage = ApiSubscribeMessage,
|
||||||
|
ApiSerializeResponse = ApiSerializeResponse,
|
||||||
|
ApiRefPatchResponse = ApiRefPatchResponse,
|
||||||
ApiValue = ApiValue,
|
ApiValue = ApiValue,
|
||||||
RbxId = RbxId,
|
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 Http = require(Packages.Http)
|
||||||
local Promise = require(Packages.Promise)
|
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)
|
local function compare(a, b)
|
||||||
if a > b then
|
if a > b then
|
||||||
@@ -88,14 +102,26 @@ function Version.display(version)
|
|||||||
return output
|
return output
|
||||||
end
|
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: {
|
function Version.retrieveLatestCompatible(options: {
|
||||||
version: { number },
|
version: { number },
|
||||||
includePrereleases: boolean?,
|
includePrereleases: boolean?,
|
||||||
}): {
|
}): LatestReleaseInfo?
|
||||||
version: { number },
|
if Version._cachedLatestCompatible and os.clock() - Version._cachedLatestCompatible.timestamp < 60 * 60 * 24 then
|
||||||
prerelease: boolean,
|
Log.debug("Using cached latest compatible version")
|
||||||
publishedUnixTimestamp: number,
|
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")
|
local success, releases = Http.get("https://api.github.com/repos/rojo-rbx/rojo/releases?per_page=10")
|
||||||
:andThen(function(response)
|
:andThen(function(response)
|
||||||
if response.code >= 400 then
|
if response.code >= 400 then
|
||||||
@@ -114,7 +140,7 @@ function Version.retrieveLatestCompatible(options: {
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Iterate through releases, looking for the latest compatible version
|
-- Iterate through releases, looking for the latest compatible version
|
||||||
local latestCompatible = nil
|
local latestCompatible: LatestReleaseInfo? = nil
|
||||||
for _, release in releases do
|
for _, release in releases do
|
||||||
-- Skip prereleases if they are not requested
|
-- Skip prereleases if they are not requested
|
||||||
if (not options.includePrereleases) and release.prerelease then
|
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
|
-- 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
|
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
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Cache the latest compatible version
|
||||||
|
Version._cachedLatestCompatible = {
|
||||||
|
value = latestCompatible,
|
||||||
|
timestamp = os.clock(),
|
||||||
|
}
|
||||||
|
|
||||||
return latestCompatible
|
return latestCompatible
|
||||||
end
|
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
|
return Version
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ return function(TestEZ)
|
|||||||
local Rojo = script.Parent.Parent
|
local Rojo = script.Parent.Parent
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
TestEZ.TestBootstrap:run({ Rojo.Plugin, Packages.Http, Packages.Log, Packages.RbxDom })
|
return TestEZ.TestBootstrap:run({ Rojo.Plugin, Packages.Http, Packages.Log, Packages.RbxDom })
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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": {
|
"ReplicatedStorage": {
|
||||||
"Rojo": {
|
"Rojo": {
|
||||||
"$path": "default.project.json"
|
"$path": "../plugin.project.json"
|
||||||
},
|
},
|
||||||
|
|
||||||
"Packages": {
|
"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">
|
<Item class="Model" referent="0">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">init_meta_class_name</string>
|
<string name="Name">init_meta_class_name</string>
|
||||||
|
<bool name="NeedsPivotMigration">false</bool>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
</roblox>
|
</roblox>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/build.rs
|
source: tests/tests/build.rs
|
||||||
expression: contents
|
expression: contents
|
||||||
|
|
||||||
---
|
---
|
||||||
<roblox version="4">
|
<roblox version="4">
|
||||||
<Item class="Folder" referent="0">
|
<Item class="Folder" referent="0">
|
||||||
@@ -25,6 +24,7 @@ expression: contents
|
|||||||
<R21>0</R21>
|
<R21>0</R21>
|
||||||
<R22>1</R22>
|
<R22>1</R22>
|
||||||
</CoordinateFrame>
|
</CoordinateFrame>
|
||||||
|
<bool name="NeedsPivotMigration">false</bool>
|
||||||
<Ref name="PrimaryPart">null</Ref>
|
<Ref name="PrimaryPart">null</Ref>
|
||||||
<BinaryString name="Tags"></BinaryString>
|
<BinaryString name="Tags"></BinaryString>
|
||||||
</Properties>
|
</Properties>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/build.rs
|
source: tests/tests/build.rs
|
||||||
expression: contents
|
expression: contents
|
||||||
|
|
||||||
---
|
---
|
||||||
<roblox version="4">
|
<roblox version="4">
|
||||||
<Item class="DataModel" referent="0">
|
<Item class="DataModel" referent="0">
|
||||||
@@ -22,6 +21,7 @@ expression: contents
|
|||||||
<Item class="Workspace" referent="2">
|
<Item class="Workspace" referent="2">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">Workspace</string>
|
<string name="Name">Workspace</string>
|
||||||
|
<bool name="NeedsPivotMigration">false</bool>
|
||||||
</Properties>
|
</Properties>
|
||||||
<Item class="BoolValue" referent="3">
|
<Item class="BoolValue" referent="3">
|
||||||
<Properties>
|
<Properties>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: redactions.redacted_yaml(info)
|
expression: redactions.redacted_yaml(info)
|
||||||
|
|
||||||
---
|
---
|
||||||
expectedPlaceIds: ~
|
expectedPlaceIds: ~
|
||||||
gameId: ~
|
gameId: ~
|
||||||
@@ -11,4 +10,4 @@ protocolVersion: 4
|
|||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
unexpectedPlaceIds: ~
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: redactions.redacted_yaml(info)
|
expression: redactions.redacted_yaml(info)
|
||||||
|
|
||||||
---
|
---
|
||||||
expectedPlaceIds: ~
|
expectedPlaceIds: ~
|
||||||
gameId: ~
|
gameId: ~
|
||||||
@@ -11,4 +10,4 @@ protocolVersion: 4
|
|||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
unexpectedPlaceIds: ~
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: redactions.redacted_yaml(info)
|
expression: redactions.redacted_yaml(info)
|
||||||
|
|
||||||
---
|
---
|
||||||
expectedPlaceIds: ~
|
expectedPlaceIds: ~
|
||||||
gameId: ~
|
gameId: ~
|
||||||
@@ -11,4 +10,4 @@ protocolVersion: 4
|
|||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
unexpectedPlaceIds: ~
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||||
|
|
||||||
---
|
---
|
||||||
instances:
|
instances:
|
||||||
id-2:
|
id-2:
|
||||||
@@ -22,7 +21,8 @@ instances:
|
|||||||
ignoreUnknownInstances: false
|
ignoreUnknownInstances: false
|
||||||
Name: test
|
Name: test
|
||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties: {}
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
messageCursor: 1
|
messageCursor: 1
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: redactions.redacted_yaml(info)
|
expression: redactions.redacted_yaml(info)
|
||||||
|
|
||||||
---
|
---
|
||||||
expectedPlaceIds: ~
|
expectedPlaceIds: ~
|
||||||
gameId: ~
|
gameId: ~
|
||||||
@@ -11,4 +10,4 @@ protocolVersion: 4
|
|||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
unexpectedPlaceIds: ~
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||||
|
|
||||||
---
|
---
|
||||||
messageCursor: 1
|
messageCursor: 1
|
||||||
messages:
|
messages:
|
||||||
@@ -14,8 +13,9 @@ messages:
|
|||||||
ignoreUnknownInstances: false
|
ignoreUnknownInstances: false
|
||||||
Name: test
|
Name: test
|
||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties: {}
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
removed: []
|
removed: []
|
||||||
updated: []
|
updated: []
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
source: tests/tests/serve.rs
|
||||||
|
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||||
|
---
|
||||||
|
instances:
|
||||||
|
id-2:
|
||||||
|
Children: []
|
||||||
|
ClassName: Attachment
|
||||||
|
Id: id-2
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: true
|
||||||
|
Name: forced_parent
|
||||||
|
Parent: "00000000000000000000000000000000"
|
||||||
|
Properties: {}
|
||||||
|
messageCursor: 0
|
||||||
|
sessionId: id-1
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
source: tests/tests/serve.rs
|
||||||
|
expression: redactions.redacted_yaml(&info)
|
||||||
|
---
|
||||||
|
expectedPlaceIds: ~
|
||||||
|
gameId: ~
|
||||||
|
placeId: ~
|
||||||
|
projectName: forced_parent
|
||||||
|
protocolVersion: 4
|
||||||
|
rootInstanceId: id-2
|
||||||
|
serverVersion: "[server-version]"
|
||||||
|
sessionId: id-1
|
||||||
|
unexpectedPlaceIds: ~
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
source: tests/tests/serve.rs
|
||||||
|
expression: model
|
||||||
|
---
|
||||||
|
<roblox version="4">
|
||||||
|
<Item class="Folder" referent="0">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">Folder</string>
|
||||||
|
</Properties>
|
||||||
|
<Item class="ObjectValue" referent="1">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">id-2</string>
|
||||||
|
<Ref name="Value">2</Ref>
|
||||||
|
</Properties>
|
||||||
|
<Item class="Part" referent="3">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">Part</string>
|
||||||
|
</Properties>
|
||||||
|
<Item class="Attachment" referent="2">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">forced_parent</string>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</roblox>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
source: tests/tests/serve.rs
|
||||||
|
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||||
|
---
|
||||||
|
instances:
|
||||||
|
id-2:
|
||||||
|
Children:
|
||||||
|
- id-3
|
||||||
|
ClassName: DataModel
|
||||||
|
Id: id-2
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: true
|
||||||
|
Name: meshpart
|
||||||
|
Parent: "00000000000000000000000000000000"
|
||||||
|
Properties: {}
|
||||||
|
id-3:
|
||||||
|
Children:
|
||||||
|
- id-4
|
||||||
|
- id-5
|
||||||
|
ClassName: Workspace
|
||||||
|
Id: id-3
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: true
|
||||||
|
Name: Workspace
|
||||||
|
Parent: id-2
|
||||||
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
|
id-4:
|
||||||
|
Children: []
|
||||||
|
ClassName: ObjectValue
|
||||||
|
Id: id-4
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: true
|
||||||
|
Name: ObjectValue
|
||||||
|
Parent: id-3
|
||||||
|
Properties:
|
||||||
|
Attributes:
|
||||||
|
Attributes:
|
||||||
|
Rojo_Target_Value:
|
||||||
|
String: sword
|
||||||
|
Value:
|
||||||
|
Ref: id-5
|
||||||
|
id-5:
|
||||||
|
Children: []
|
||||||
|
ClassName: MeshPart
|
||||||
|
Id: id-5
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: true
|
||||||
|
Name: Sword
|
||||||
|
Parent: id-3
|
||||||
|
Properties:
|
||||||
|
MeshId:
|
||||||
|
ContentId: "rbxasset://fonts/sword.mesh"
|
||||||
|
TextureID:
|
||||||
|
ContentId: "rbxasset://textures/SwordTexture.png"
|
||||||
|
messageCursor: 0
|
||||||
|
sessionId: id-1
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
source: tests/tests/serve.rs
|
||||||
|
expression: redactions.redacted_yaml(&info)
|
||||||
|
---
|
||||||
|
expectedPlaceIds: ~
|
||||||
|
gameId: ~
|
||||||
|
placeId: ~
|
||||||
|
projectName: meshpart
|
||||||
|
protocolVersion: 4
|
||||||
|
rootInstanceId: id-2
|
||||||
|
serverVersion: "[server-version]"
|
||||||
|
sessionId: id-1
|
||||||
|
unexpectedPlaceIds: ~
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
source: tests/tests/serve.rs
|
||||||
|
expression: model
|
||||||
|
---
|
||||||
|
<roblox version="4">
|
||||||
|
<Item class="Folder" referent="0">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">Folder</string>
|
||||||
|
</Properties>
|
||||||
|
<Item class="ObjectValue" referent="1">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">id-5</string>
|
||||||
|
<BinaryString name="AttributesSerialize"></BinaryString>
|
||||||
|
<Ref name="Value">2</Ref>
|
||||||
|
</Properties>
|
||||||
|
<Item class="MeshPart" referent="2">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">Sword</string>
|
||||||
|
<Content name="MeshContent">
|
||||||
|
<uri>rbxasset://fonts/sword.mesh</uri>
|
||||||
|
</Content>
|
||||||
|
<Content name="TextureContent">
|
||||||
|
<uri>rbxasset://textures/SwordTexture.png</uri>
|
||||||
|
</Content>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
<Item class="ObjectValue" referent="3">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">id-4</string>
|
||||||
|
<BinaryString name="AttributesSerialize"></BinaryString>
|
||||||
|
<Ref name="Value">4</Ref>
|
||||||
|
</Properties>
|
||||||
|
<Item class="ObjectValue" referent="4">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">ObjectValue</string>
|
||||||
|
<BinaryString name="AttributesSerialize">AQAAABEAAABSb2pvX1RhcmdldF9WYWx1ZQIFAAAAc3dvcmQ=</BinaryString>
|
||||||
|
<Ref name="Value">null</Ref>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</roblox>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
source: tests/tests/serve.rs
|
||||||
|
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||||
|
---
|
||||||
|
instances:
|
||||||
|
id-2:
|
||||||
|
Children:
|
||||||
|
- id-3
|
||||||
|
ClassName: DataModel
|
||||||
|
Id: id-2
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: true
|
||||||
|
Name: pivot_migration
|
||||||
|
Parent: "00000000000000000000000000000000"
|
||||||
|
Properties: {}
|
||||||
|
id-3:
|
||||||
|
Children:
|
||||||
|
- id-4
|
||||||
|
- id-5
|
||||||
|
- id-6
|
||||||
|
ClassName: Workspace
|
||||||
|
Id: id-3
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: true
|
||||||
|
Name: Workspace
|
||||||
|
Parent: id-2
|
||||||
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
|
id-4:
|
||||||
|
Children: []
|
||||||
|
ClassName: Model
|
||||||
|
Id: id-4
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: true
|
||||||
|
Name: Model
|
||||||
|
Parent: id-3
|
||||||
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
|
id-5:
|
||||||
|
Children: []
|
||||||
|
ClassName: Tool
|
||||||
|
Id: id-5
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: false
|
||||||
|
Name: Tool
|
||||||
|
Parent: id-3
|
||||||
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
|
id-6:
|
||||||
|
Children: []
|
||||||
|
ClassName: Actor
|
||||||
|
Id: id-6
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: true
|
||||||
|
Name: Actor
|
||||||
|
Parent: id-3
|
||||||
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
|
messageCursor: 1
|
||||||
|
sessionId: id-1
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
source: tests/tests/serve.rs
|
||||||
|
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||||
|
---
|
||||||
|
messageCursor: 1
|
||||||
|
messages:
|
||||||
|
- added:
|
||||||
|
id-6:
|
||||||
|
Children: []
|
||||||
|
ClassName: Actor
|
||||||
|
Id: id-6
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: true
|
||||||
|
Name: Actor
|
||||||
|
Parent: id-3
|
||||||
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
|
removed: []
|
||||||
|
updated:
|
||||||
|
- changedClassName: ~
|
||||||
|
changedMetadata:
|
||||||
|
ignoreUnknownInstances: true
|
||||||
|
changedName: ~
|
||||||
|
changedProperties: {}
|
||||||
|
id: id-3
|
||||||
|
sessionId: id-1
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: redactions.redacted_yaml(info)
|
expression: redactions.redacted_yaml(info)
|
||||||
|
|
||||||
---
|
---
|
||||||
expectedPlaceIds: ~
|
expectedPlaceIds: ~
|
||||||
gameId: ~
|
gameId: ~
|
||||||
@@ -11,4 +10,4 @@ protocolVersion: 4
|
|||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
unexpectedPlaceIds: ~
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
assertion_line: 316
|
|
||||||
expression: redactions.redacted_yaml(info)
|
expression: redactions.redacted_yaml(info)
|
||||||
---
|
---
|
||||||
expectedPlaceIds: ~
|
expectedPlaceIds: ~
|
||||||
@@ -11,4 +10,4 @@ protocolVersion: 4
|
|||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
unexpectedPlaceIds: ~
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
assertion_line: 335
|
|
||||||
expression: redactions.redacted_yaml(info)
|
expression: redactions.redacted_yaml(info)
|
||||||
---
|
---
|
||||||
expectedPlaceIds: ~
|
expectedPlaceIds: ~
|
||||||
@@ -11,4 +10,4 @@ protocolVersion: 4
|
|||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
unexpectedPlaceIds: ~
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
assertion_line: 351
|
|
||||||
expression: redactions.redacted_yaml(info)
|
expression: redactions.redacted_yaml(info)
|
||||||
---
|
---
|
||||||
expectedPlaceIds: ~
|
expectedPlaceIds: ~
|
||||||
@@ -11,4 +10,4 @@ protocolVersion: 4
|
|||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
unexpectedPlaceIds: ~
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
source: tests/tests/serve.rs
|
||||||
|
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||||
|
---
|
||||||
|
instances:
|
||||||
|
id-2:
|
||||||
|
Children:
|
||||||
|
- id-3
|
||||||
|
ClassName: DataModel
|
||||||
|
Id: id-2
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: true
|
||||||
|
Name: pivot_migration
|
||||||
|
Parent: "00000000000000000000000000000000"
|
||||||
|
Properties: {}
|
||||||
|
id-3:
|
||||||
|
Children:
|
||||||
|
- id-4
|
||||||
|
- id-5
|
||||||
|
ClassName: Workspace
|
||||||
|
Id: id-3
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: true
|
||||||
|
Name: Workspace
|
||||||
|
Parent: id-2
|
||||||
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
|
id-4:
|
||||||
|
Children: []
|
||||||
|
ClassName: Model
|
||||||
|
Id: id-4
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: true
|
||||||
|
Name: Model
|
||||||
|
Parent: id-3
|
||||||
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
|
id-5:
|
||||||
|
Children: []
|
||||||
|
ClassName: Tool
|
||||||
|
Id: id-5
|
||||||
|
Metadata:
|
||||||
|
ignoreUnknownInstances: false
|
||||||
|
Name: Tool
|
||||||
|
Parent: id-3
|
||||||
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
|
messageCursor: 0
|
||||||
|
sessionId: id-1
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
source: tests/tests/serve.rs
|
||||||
|
expression: redactions.redacted_yaml(info)
|
||||||
|
---
|
||||||
|
expectedPlaceIds: ~
|
||||||
|
gameId: ~
|
||||||
|
placeId: ~
|
||||||
|
projectName: pivot_migration
|
||||||
|
protocolVersion: 4
|
||||||
|
rootInstanceId: id-2
|
||||||
|
serverVersion: "[server-version]"
|
||||||
|
sessionId: id-1
|
||||||
|
unexpectedPlaceIds: ~
|
||||||
@@ -31,6 +31,8 @@ instances:
|
|||||||
Attributes:
|
Attributes:
|
||||||
Rojo_Target_PrimaryPart:
|
Rojo_Target_PrimaryPart:
|
||||||
String: project target
|
String: project target
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
PrimaryPart:
|
PrimaryPart:
|
||||||
Ref: id-9
|
Ref: id-9
|
||||||
id-2:
|
id-2:
|
||||||
@@ -55,7 +57,9 @@ instances:
|
|||||||
ignoreUnknownInstances: true
|
ignoreUnknownInstances: true
|
||||||
Name: Workspace
|
Name: Workspace
|
||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties: {}
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
id-4:
|
id-4:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: ObjectValue
|
ClassName: ObjectValue
|
||||||
@@ -124,6 +128,8 @@ instances:
|
|||||||
Attributes:
|
Attributes:
|
||||||
Rojo_Target_PrimaryPart:
|
Rojo_Target_PrimaryPart:
|
||||||
String: model target 2
|
String: model target 2
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
PrimaryPart:
|
PrimaryPart:
|
||||||
Ref: id-7
|
Ref: id-7
|
||||||
id-9:
|
id-9:
|
||||||
@@ -138,4 +144,3 @@ instances:
|
|||||||
Properties: {}
|
Properties: {}
|
||||||
messageCursor: 1
|
messageCursor: 1
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ instances:
|
|||||||
ignoreUnknownInstances: true
|
ignoreUnknownInstances: true
|
||||||
Name: Workspace
|
Name: Workspace
|
||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties: {}
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
id-4:
|
id-4:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: ObjectValue
|
ClassName: ObjectValue
|
||||||
@@ -104,6 +106,8 @@ instances:
|
|||||||
Attributes:
|
Attributes:
|
||||||
Rojo_Target_PrimaryPart:
|
Rojo_Target_PrimaryPart:
|
||||||
String: model target
|
String: model target
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
PrimaryPart:
|
PrimaryPart:
|
||||||
Ref: id-7
|
Ref: id-7
|
||||||
id-9:
|
id-9:
|
||||||
@@ -118,4 +122,3 @@ instances:
|
|||||||
Properties: {}
|
Properties: {}
|
||||||
messageCursor: 0
|
messageCursor: 0
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ protocolVersion: 4
|
|||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
unexpectedPlaceIds: ~
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ instances:
|
|||||||
ignoreUnknownInstances: true
|
ignoreUnknownInstances: true
|
||||||
Name: Workspace
|
Name: Workspace
|
||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties: {}
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
id-4:
|
id-4:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: ObjectValue
|
ClassName: ObjectValue
|
||||||
@@ -104,8 +106,12 @@ instances:
|
|||||||
Attributes:
|
Attributes:
|
||||||
Rojo_Target_PrimaryPart:
|
Rojo_Target_PrimaryPart:
|
||||||
String: model target
|
String: model target
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
PrimaryPart:
|
PrimaryPart:
|
||||||
Ref: id-7
|
Ref: id-7
|
||||||
|
Scale:
|
||||||
|
Float32: 1
|
||||||
id-9:
|
id-9:
|
||||||
Children:
|
Children:
|
||||||
- id-10
|
- id-10
|
||||||
@@ -116,5 +122,5 @@ instances:
|
|||||||
Name: ProjectTarget
|
Name: ProjectTarget
|
||||||
Parent: id-3
|
Parent: id-3
|
||||||
Properties: {}
|
Properties: {}
|
||||||
messageCursor: 0
|
messageCursor: 1
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ instances:
|
|||||||
ignoreUnknownInstances: true
|
ignoreUnknownInstances: true
|
||||||
Name: Workspace
|
Name: Workspace
|
||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties: {}
|
Properties:
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
id-4:
|
id-4:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: ObjectValue
|
ClassName: ObjectValue
|
||||||
@@ -104,6 +106,8 @@ instances:
|
|||||||
Attributes:
|
Attributes:
|
||||||
Rojo_Target_PrimaryPart:
|
Rojo_Target_PrimaryPart:
|
||||||
String: model target
|
String: model target
|
||||||
|
NeedsPivotMigration:
|
||||||
|
Bool: false
|
||||||
PrimaryPart:
|
PrimaryPart:
|
||||||
Ref: id-7
|
Ref: id-7
|
||||||
id-9:
|
id-9:
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ protocolVersion: 4
|
|||||||
rootInstanceId: id-2
|
rootInstanceId: id-2
|
||||||
serverVersion: "[server-version]"
|
serverVersion: "[server-version]"
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
unexpectedPlaceIds: ~
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user