Compare commits

..

48 Commits

Author SHA1 Message Date
Micah
47db569c52 Correct my own mistake and move plugin test to its own step 2025-09-23 21:19:26 -07:00
Micah
1d3f8c8e9d Run tests in CI (this is the part where I have to try at least twice) 2025-09-23 21:13:24 -07:00
Micah
a894313a4b Make script exit with code 1 when tests fail 2025-09-23 21:09:13 -07:00
Micah
7f73ae80dc Add script for running plugin tests locally
They current fail
2025-09-23 21:02:23 -07:00
Micah
80a381dbb1 Use SerializationService as a fallback for when patch application fails (#1030) 2025-09-21 15:09:20 -07:00
KAS
59e36491a5 Fix a grammar error and a typo (#1113) 2025-09-16 11:00:34 -07:00
Micah
c1326ba06e Add Arm64 builds to CI/Releases + build on ubuntu 22.04 (#1098) 2025-08-30 14:10:59 -07:00
Micah
e2633126ee Change Foreman to Rokit in CONTRIBUTING.md (#1110) 2025-08-30 13:42:52 -07:00
Micah
5f33435f3c Move to using Rokit, update tools, and don't install unnecessary tools (#1109) 2025-08-29 18:45:15 -07:00
boatbomber
54e0ff230b Improvements to sync reminder UX (#1096) 2025-08-28 17:15:34 -07:00
wad
4e9e6233ff fix: apply gameId and placeId only after initial sync (#1104) 2025-08-15 18:12:36 -07:00
Micah
0056849b51 Put Rojo version in crash message (#1101) 2025-08-13 15:46:08 -07:00
ffrostfall
2ddb21ec5f Add option for emitting absolute paths to rojo sourcemap (#1092)
Co-authored-by: Micah <micah@uplift.games>
2025-08-04 11:33:35 -07:00
Micah
a4eb65ca3f Add YAML middleware that behaves like TOML and JSON (#1093) 2025-08-02 20:58:13 -07:00
Sebastian Stachowicz
3002d250a1 Fix Table diff colors (#1084) 2025-07-31 19:36:03 -07:00
Micah
9598553e5d Normalize paths in sourcemap generation (#1085) 2025-07-31 09:19:57 -07:00
Sebastian Stachowicz
7f68d9887b Fixed nil -> nil props showing up as failing in patch visualizer (plugin) (#1081) 2025-07-25 15:27:11 -07:00
Micah
e092a7301f Change background color of web UI to gray (#1080) 2025-07-25 15:04:42 -07:00
Cameron Campbell
6dfdfbe514 Cache Rust Dependencies in release.yml. (#1079) 2025-07-22 16:13:23 -07:00
morosanu
7860f2717f Fix auto connect for play mode (#1066) 2025-07-22 15:12:16 -07:00
boatbomber
60f19df9a0 Show update indicator on version header (#1069) 2025-06-21 02:53:45 +00:00
boatbomber
951f0cda0b Show the plugin version on the Error page (#1068) 2025-06-20 18:28:04 -07:00
Micah
227042d6b1 Add current maintainers to author field of Cargo.toml files (#1053) 2025-05-21 20:55:39 -07:00
Micah
b2c4f550ee Release v7.5.1 (#1035) 2025-04-25 13:56:01 -07:00
Ken Loeffler
4ddbefa88f Change release build linux version to ubuntu-latest (#1034) 2025-04-25 20:04:43 +01:00
Ken Loeffler
d935115591 Release 7.5.0 (#1033) 2025-04-25 19:46:16 +01:00
Cameron Campbell
bd2ea42732 Fixes issues with refs in the plugin. (#1005) 2025-04-18 08:44:11 -07:00
Micah
3bac38ee34 Re-add the hack to write NeedsPivotMigration as false for models (#1027) 2025-04-16 15:03:09 -07:00
Micah
a7a4f6d8f2 Update rbx-dom-lua database to latest version (#1029) 2025-04-13 07:42:41 -07:00
Micah
80b6facbd3 Add missing CHANGELOG entry for 7.4.4 (#1025) 2025-04-07 16:22:08 -07:00
Parritz
7dee898400 Add place ID blacklist config (#1021) 2025-04-03 08:37:40 -07:00
Micah
4c4b2dbe17 Add legacy and runContext script sync rule middlewares (#909) 2025-04-02 12:47:27 -07:00
Sasial
73ed5ae697 Add Support for Plugin Scripts (#1008) 2025-04-02 11:37:49 -07:00
Micah
833320de64 Update rbx-dom (#1023) 2025-04-02 11:32:27 -07:00
boatbomber
0d6ff8ef8a Improve notification layout (#997) 2025-01-13 16:06:49 -08:00
Jack T
55a207a275 Fix clippy lint warnings (#1004) 2025-01-13 10:07:53 -08:00
Jack T
f33d1f1cc4 Ignore .git directory when building VfsSnapshot in build script (#1002) 2025-01-01 01:38:34 -08:00
boatbomber
19ca2b12fc Add locked tooltip (#998)
Adds the ability to define descriptive tooltips for settings when they
are locked.


![image](https://github.com/user-attachments/assets/5d5778c8-911b-4358-b4e6-f0389270ad76)


Makes some minor improvements to tooltip layout logic as well.
2024-12-28 15:03:11 -08:00
boatbomber
b7d3394464 Plugin dev ux improvements (#992)
Co-authored-by: kennethloeffler <kenloef@gmail.com>
2024-11-10 15:53:58 -08:00
boatbomber
8c33100d7a Use FontFace and consistent text sizing (#988) 2024-11-09 12:05:57 +00:00
Kenneth Loeffler
80c406f196 Fix returning NoProjectFound for any project load error (#985)
In #917, we accidentally changed ServeSession::new's project loading
logic so that it always returns `ServeSession::ProjectNotFound` if the
load fails for any reason. This PR fixes this so that it returns the
right error when there is an error loading the project, and moves the
`NoProjectFound` error to `project::Error`, since I think it makes more
sense there.
2024-11-08 08:40:32 +00:00
Kenneth Loeffler
bc2c76e5e2 Use 7.5.0-prealpha for master branch version, not 7.4.4
<p dir="auto">in <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="2636858743" data-permission-text="Title is private" data-url="https://github.com/rojo-rbx/rojo/issues/989" data-hovercard-type="pull_request" data-hovercard-url="/rojo-rbx/rojo/pull/989/hovercard" href="https://github.com/rojo-rbx/rojo/pull/989">#989</a>, we changed Rojo's version number on the master branch to 7.4.4. This is a little odd, because 7.4.4 is already released, is diverged from the master branch, and we are not working towards 7.4.4 on the master branch. If we're going to spend time on this, I think we should use a more appropriate version number.</p>
<p dir="auto">This PR changes the version number to 7.5.0-prealpha, since Rojo's master branch is currently undergoing development towards 7.5.0. We will most likely <strong>not</strong> be making a release of this version - the only intent is better clarity for those running Rojo's latest master.</p>
2024-11-06 15:59:03 +00:00
boatbomber
4a7bddbc09 Update version in master branch (#989) 2024-11-05 18:10:24 -08:00
boatbomber
e316fdbaef Make sync reminder more detailed (#987) 2024-11-05 22:47:07 +00:00
Micah
34106f470f Remove maplit dependency and stop using a macro for hashmaps (#982) 2024-10-31 11:56:54 -07:00
Micah
d9ab0e7de8 Support $schema in JSON structures (#974) 2024-10-24 10:55:51 -07:00
Micah
5ca1573e2e Correct mistake in build command docs (#977) 2024-10-18 14:08:58 -07:00
Micah
c9ce996626 Update workflow and tooling versions (#910) 2024-09-03 15:36:36 -07:00
160 changed files with 13151 additions and 3530 deletions

2
.dir-locals.el Normal file
View File

@@ -0,0 +1,2 @@
((nil . ((eglot-luau-rojo-project-path . "plugin.project.json")
(eglot-luau-rojo-sourcemap-enabled . 't))))

View File

@@ -11,12 +11,12 @@ 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
with: with:
fileName: CHANGELOG.md fileName: CHANGELOG.md
noChangelogLabel: skip changelog noChangelogLabel: skip changelog
checkNotification: Simple checkNotification: Simple
env: env:

View File

@@ -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') }}

View File

@@ -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,12 +53,12 @@ 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 - host: linux
os: ubuntu-20.04 os: ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu target: aarch64-unknown-linux-gnu
label: linux-aarch64 label: linux-aarch64
@@ -70,7 +68,7 @@ jobs:
label: windows-x86_64 label: windows-x86_64
- host: windows - host: windows
os: windows-latest os: windows-11-arm
target: aarch64-pc-windows-msvc target: aarch64-pc-windows-msvc
label: windows-aarch64 label: windows-aarch64
@@ -98,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
@@ -127,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
View File

@@ -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
View File

@@ -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
View File

@@ -0,0 +1,5 @@
{
"aliases": {
"lune": "~/.lune/.typedefs/0.10.2/"
}
}

112
.lune/test-plugin.luau Normal file
View 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
View 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
View File

@@ -0,0 +1,4 @@
{
"luau-lsp.sourcemap.rojoProjectFile": "plugin.project.json",
"luau-lsp.sourcemap.autogenerate": true
}

View File

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

View File

@@ -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
View File

@@ -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",
]

View File

@@ -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"

View File

@@ -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"

View File

@@ -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%;

View File

@@ -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");

View File

@@ -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"

View File

@@ -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;

View File

@@ -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"
} }
} }
} }

View File

@@ -1 +1 @@
7.4.0 7.5.1

View File

@@ -70,7 +70,7 @@ local function debugImpl(buffer, value, extendedForm)
elseif valueType == "table" then elseif valueType == "table" then
local valueMeta = getmetatable(value) local valueMeta = getmetatable(value)
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
-- This type implement's the metamethod we made up to line up with -- This type implement's the metamethod we made up to line up with
-- Rust's 'Debug' trait. -- Rust's 'Debug' trait.
@@ -242,4 +242,4 @@ return {
debugOutputBuffer = debugOutputBuffer, debugOutputBuffer = debugOutputBuffer,
fmt = fmt, fmt = fmt,
debugify = debugify, debugify = debugify,
} }

View File

@@ -31,4 +31,4 @@ function Response:json()
return HttpService:JSONDecode(self.body) return HttpService:JSONDecode(self.body)
end end
return Response return Response

View File

@@ -2,4 +2,4 @@ return function()
it("should load", function() it("should load", function()
require(script.Parent) require(script.Parent)
end) end)
end end

View File

@@ -57,4 +57,4 @@ function Log.error(template, ...)
error(Fmt.fmt(template, ...)) error(Fmt.fmt(template, ...))
end end
return Log return Log

View File

@@ -2,4 +2,4 @@ return function()
it("should load", function() it("should load", function()
require(script.Parent) require(script.Parent)
end) end)
end end

View File

@@ -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)

View File

@@ -5,6 +5,7 @@ Error.Kind = {
UnknownProperty = "UnknownProperty", UnknownProperty = "UnknownProperty",
PropertyNotReadable = "PropertyNotReadable", PropertyNotReadable = "PropertyNotReadable",
PropertyNotWritable = "PropertyNotWritable", PropertyNotWritable = "PropertyNotWritable",
CannotParseBinaryString = "CannotParseBinaryString",
Roblox = "Roblox", Roblox = "Roblox",
} }

View File

@@ -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": [

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,22 +43,24 @@ function CodeLabel:updateHighlights()
end end
function CodeLabel:render() function CodeLabel:render()
return e("TextLabel", { return Theme.with(function(theme)
Size = self.props.size, return e("TextLabel", {
Position = self.props.position, Size = self.props.size,
Text = self.props.text, Position = self.props.position,
BackgroundTransparency = 1, Text = self.props.text,
Font = Enum.Font.RobotoMono, BackgroundTransparency = 1,
TextSize = 16, FontFace = theme.Font.Code,
TextXAlignment = Enum.TextXAlignment.Left, TextSize = theme.TextSize.Code,
TextYAlignment = Enum.TextYAlignment.Top, TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = Color3.fromRGB(255, 255, 255), TextYAlignment = Enum.TextYAlignment.Top,
[Roact.Ref] = self.labelRef, TextColor3 = Color3.fromRGB(255, 255, 255),
}, { [Roact.Ref] = self.labelRef,
SyntaxHighlights = e("Folder", { }, {
[Roact.Ref] = self.highlightsRef, SyntaxHighlights = e("Folder", {
}), [Roact.Ref] = self.highlightsRef,
}) }),
})
end)
end end
return CodeLabel return CodeLabel

View File

@@ -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),
}), }),

View File

@@ -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", {

View File

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

View File

@@ -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()
task.cancel(self.timeout) if self.timeout and coroutine.status(self.timeout) ~= "dead" then
task.cancel(self.timeout)
end
end end
function Notification:render() function Notification:render()
@@ -86,51 +85,49 @@ 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 buttonsX = 0
if self.props.actions then
local count = 0
for key, action in self.props.actions do
actionButtons[key] = e(TextButton, {
text = action.text,
style = action.style,
onClick = function()
self:dismiss()
if action.onClick then
local success, err = pcall(action.onClick, self)
if not success then
Log.warn("Error in notification action: " .. tostring(err))
end
end
end,
layoutOrder = -action.layoutOrder,
transparency = transparency,
})
local actionButtons = {} buttonsX += getTextBoundsAsync(action.text, theme.Font.Main, theme.TextSize.Large, math.huge).X + (theme.TextSize.Body * 2)
local buttonsX = 0
if self.props.actions then
local count = 0
for key, action in self.props.actions do
actionButtons[key] = e(TextButton, {
text = action.text,
style = action.style,
onClick = function()
local success, err = pcall(action.onClick, self)
if not success then
Log.warn("Error in notification action: " .. tostring(err))
end
end,
layoutOrder = -action.layoutOrder,
transparency = transparency,
})
buttonsX += TextService:GetTextSize( count += 1
action.text, end
18,
Enum.Font.GothamMedium,
Vector2.new(math.huge, math.huge)
).X + 30
count += 1 buttonsX += (count - 1) * 5
end end
buttonsX += (count - 1) * 5 local paddingY, logoSize = 20, 32
end local actionsY = if self.props.actions then 37 else 0
local textXSpace = math.max(250, buttonsX) + 35
local textBounds = getTextBoundsAsync(self.props.text, theme.Font.Main, theme.TextSize.Body, textXSpace)
local contentX = math.max(textBounds.X, buttonsX)
local paddingY, logoSize = 20, 32 local size = self.binding:map(function(value)
local actionsY = if self.props.actions then 35 else 0 return UDim2.fromOffset(
local contentX = math.max(textBounds.X, buttonsX) (35 + 40 + contentX) * value,
5 + actionsY + paddingY + math.max(logoSize, textBounds.Y)
)
end)
local size = self.binding:map(function(value)
return UDim2.fromOffset(
(35 + 40 + contentX) * value,
5 + actionsY + paddingY + math.max(logoSize, textBounds.Y)
)
end)
return Theme.with(function(theme)
return e("TextButton", { 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

View 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

View File

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

View File

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

View File

@@ -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,
}) })

View File

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

View File

@@ -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,
}), }),
}), }),

View File

@@ -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,
}), }),

View File

@@ -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,
}), }),

View File

@@ -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,46 +12,48 @@ local SlicedImage = require(Plugin.App.Components.SlicedImage)
local e = Roact.createElement local e = Roact.createElement
return function(props) return function(props)
return e(SlicedImage, { return Theme.with(function(theme)
slice = Assets.Slices.RoundedBackground, return e(SlicedImage, {
color = props.color, slice = Assets.Slices.RoundedBackground,
transparency = props.transparency:map(function(transparency) color = props.color,
return 0.9 + (0.1 * transparency) transparency = props.transparency:map(function(transparency)
end), return 0.9 + (0.1 * transparency)
layoutOrder = props.layoutOrder, end),
position = props.position, layoutOrder = props.layoutOrder,
anchorPoint = props.anchorPoint, position = props.position,
size = UDim2.new(0, 0, 0, 16), anchorPoint = props.anchorPoint,
automaticSize = Enum.AutomaticSize.X, size = UDim2.new(0, 0, 0, theme.TextSize.Medium),
}, { automaticSize = Enum.AutomaticSize.X,
Padding = e("UIPadding", { }, {
PaddingLeft = UDim.new(0, 4), Padding = e("UIPadding", {
PaddingRight = UDim.new(0, 4), PaddingLeft = UDim.new(0, 4),
PaddingTop = UDim.new(0, 2), PaddingRight = UDim.new(0, 4),
PaddingBottom = UDim.new(0, 2), PaddingTop = UDim.new(0, 2),
}), PaddingBottom = UDim.new(0, 2),
Icon = if props.icon }),
then e("ImageLabel", { Icon = if props.icon
Size = UDim2.new(0, 12, 0, 12), then e("ImageLabel", {
Position = UDim2.new(0, 0, 0.5, 0), Size = UDim2.new(0, 12, 0, 12),
AnchorPoint = Vector2.new(0, 0.5), Position = UDim2.new(0, 0, 0.5, 0),
Image = props.icon, AnchorPoint = Vector2.new(0, 0.5),
Image = props.icon,
BackgroundTransparency = 1,
ImageColor3 = props.color,
ImageTransparency = props.transparency,
})
else nil,
Text = e("TextLabel", {
Text = props.text,
FontFace = theme.Font.Main,
TextSize = theme.TextSize.Small,
TextColor3 = props.color,
TextXAlignment = Enum.TextXAlignment.Center,
TextTransparency = props.transparency,
Size = UDim2.new(0, 0, 1, 0),
Position = UDim2.new(0, if props.icon then 15 else 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1, BackgroundTransparency = 1,
ImageColor3 = props.color, }),
ImageTransparency = props.transparency, })
}) end)
else nil,
Text = e("TextLabel", {
Text = props.text,
Font = Enum.Font.GothamMedium,
TextSize = 12,
TextColor3 = props.color,
TextXAlignment = Enum.TextXAlignment.Center,
TextTransparency = props.transparency,
Size = UDim2.new(0, 0, 1, 0),
Position = UDim2.new(0, if props.icon then 15 else 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
}),
})
end end

View File

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

View File

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

View File

@@ -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,50 +22,48 @@ 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(
props.Text,
16,
Enum.Font.GothamMedium,
Vector2.new(math.min(props.parentSize.X, 160), math.huge)
) + TEXT_PADDING + (Vector2.one * 2)
local trigger = props.Trigger:getValue()
local spaceBelow = props.parentSize.Y
- (trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y - Y_OVERLAP + TAIL_SIZE)
local spaceAbove = trigger.AbsolutePosition.Y + Y_OVERLAP - TAIL_SIZE
-- If there's not enough space below, and there's more space above, then show the tooltip above the trigger
local displayAbove = spaceBelow < textSize.Y and spaceAbove > spaceBelow
local X = math.clamp(props.Position.X - X_OFFSET, 0, props.parentSize.X - textSize.X)
local Y = 0
if displayAbove then
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - textSize.Y + Y_OVERLAP, 0)
else
Y = math.min(
trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP,
props.parentSize.Y - textSize.Y
)
end
return Theme.with(function(theme) return Theme.with(function(theme)
local textXSpace = math.min(props.parentSize.X, 250) - TEXT_PADDING.X
local textBounds = getTextBoundsAsync(props.Text, theme.Font.Main, theme.TextSize.Medium, textXSpace)
local contentSize = textBounds + TEXT_PADDING + (Vector2.one * 2)
local trigger = props.Trigger:getValue()
local spaceBelow = props.parentSize.Y
- (trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y - Y_OVERLAP + TAIL_SIZE)
local spaceAbove = trigger.AbsolutePosition.Y + Y_OVERLAP - TAIL_SIZE
-- If there's not enough space below, and there's more space above, then show the tooltip above the trigger
local displayAbove = spaceBelow < contentSize.Y and spaceAbove > spaceBelow
local X = math.clamp(props.Position.X - X_OFFSET, 0, math.max(props.parentSize.X - contentSize.X, 1))
local Y = 0
if displayAbove then
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - contentSize.Y + Y_OVERLAP, 0)
else
Y = math.min(
trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP,
props.parentSize.Y - contentSize.Y
)
end
return e(BorderedContainer, { 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(),

View File

@@ -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,
}), }),

View File

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

View File

@@ -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,43 +24,44 @@ function Error:init()
end end
function Error:render() function Error:render()
return e(BorderedContainer, { return Theme.with(function(theme)
size = Roact.joinBindings({ return e(BorderedContainer, {
containerSize = self.props.containerSize, size = Roact.joinBindings({
contentSize = self.contentSize, containerSize = self.props.containerSize,
}):map(function(values) contentSize = self.contentSize,
local maximumSize = values.containerSize }):map(function(values)
maximumSize -= Vector2.new(14, 14) * 2 -- Page padding local maximumSize = values.containerSize
maximumSize -= Vector2.new(0, 34 + 10) -- Buttons and spacing maximumSize -= Vector2.new(14, 14) * 2 -- Page padding
maximumSize -= Vector2.new(0, 34 + 10) -- Buttons and spacing
local outerSize = values.contentSize + ERROR_PADDING * 2 local outerSize = values.contentSize + ERROR_PADDING * 2
return UDim2.new(1, 0, 0, math.min(outerSize.Y, maximumSize.Y)) return UDim2.new(1, 0, 0, math.min(outerSize.Y, maximumSize.Y))
end),
transparency = self.props.transparency,
}, {
ScrollingFrame = e(ScrollingFrame, {
size = UDim2.new(1, 0, 1, 0),
contentSize = self.contentSize:map(function(value)
return value + ERROR_PADDING * 2
end), end),
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = self.props.layoutOrder,
[Roact.Change.AbsoluteSize] = function(object)
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
local textBounds = TextService:GetTextSize(
self.props.errorMessage,
16,
Enum.Font.Code,
Vector2.new(containerSize.X, math.huge)
)
self.setContentSize(Vector2.new(containerSize.X, textBounds.Y))
end,
}, { }, {
ErrorMessage = Theme.with(function(theme) ScrollingFrame = e(ScrollingFrame, {
return e("TextBox", { size = UDim2.new(1, 0, 1, 0),
contentSize = self.contentSize:map(function(value)
return value + ERROR_PADDING * 2
end),
transparency = self.props.transparency,
[Roact.Change.AbsoluteSize] = function(object)
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
local textBounds = getTextBoundsAsync(
self.props.errorMessage,
theme.Font.Code,
theme.TextSize.Code,
containerSize.X
)
self.setContentSize(Vector2.new(containerSize.X, textBounds.Y))
end,
}, {
ErrorMessage = e("TextBox", {
[Roact.Event.InputBegan] = function(rbx, input) [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,17 +82,17 @@ 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),
PaddingRight = UDim.new(0, ERROR_PADDING.X), PaddingRight = UDim.new(0, ERROR_PADDING.X),
PaddingTop = UDim.new(0, ERROR_PADDING.Y), PaddingTop = UDim.new(0, ERROR_PADDING.Y),
PaddingBottom = UDim.new(0, ERROR_PADDING.Y), PaddingBottom = UDim.new(0, ERROR_PADDING.Y),
}),
}), }),
}), })
}) 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, {

View File

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

View File

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

View File

@@ -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,185 +82,211 @@ function SettingsPage:render()
return layoutOrder return layoutOrder
end end
return Theme.with(function(theme) return Roact.createFragment({
theme = theme.Settings Navbar = e(Navbar, {
onBack = self.props.onBack,
return Roact.createFragment({ transparency = self.props.transparency,
Navbar = e(Navbar, { layoutOrder = layoutIncrement(),
onBack = self.props.onBack, }),
Content = e(ScrollingFrame, {
size = UDim2.new(1, 0, 1, -47),
position = UDim2.new(0, 0, 0, 47),
contentSize = self.contentSize,
transparency = self.props.transparency,
}, {
AutoReconnect = e(Setting, {
id = "autoReconnect",
name = "Auto Reconnect",
description = "Reconnect to server on place open if the served project matches the last sync to the place",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = layoutIncrement(), layoutOrder = layoutIncrement(),
}), }),
Content = e(ScrollingFrame, {
size = UDim2.new(1, 0, 1, -47), ShowNotifications = e(Setting, {
position = UDim2.new(0, 0, 0, 47), id = "showNotifications",
contentSize = self.contentSize, name = "Show Notifications",
description = "Popup notifications in viewport",
transparency = self.props.transparency, transparency = self.props.transparency,
}, { layoutOrder = layoutIncrement(),
ShowNotifications = e(Setting, { }),
id = "showNotifications",
name = "Show Notifications",
description = "Popup notifications in viewport",
transparency = self.props.transparency,
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"),
}),
ConfirmationBehavior = e(Setting, { options = syncReminderModes,
id = "confirmationBehavior", }),
name = "Confirmation Behavior",
description = "When to prompt for confirmation before syncing",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
options = confirmationBehaviors, SyncReminderPolling = e(Setting, {
}), id = "syncReminderPolling",
name = "Sync Reminder Polling",
description = "Look for available sync servers periodically",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
visible = Settings:getBindings("syncReminderMode", "showNotifications"):map(function(values)
return values.syncReminderMode ~= "None" and values.showNotifications
end),
}),
LargeChangesConfirmationThreshold = e(Setting, { ConfirmationBehavior = e(Setting, {
id = "largeChangesConfirmationThreshold", id = "confirmationBehavior",
name = "Confirmation Threshold", name = "Confirmation Behavior",
description = "How many modified instances to be considered a large change", description = "When to prompt for confirmation before syncing",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = layoutIncrement(), layoutOrder = layoutIncrement(),
visible = Settings:getBinding("confirmationBehavior"):map(function(value)
return value == "Large Changes" options = confirmationBehaviors,
}),
LargeChangesConfirmationThreshold = e(Setting, {
id = "largeChangesConfirmationThreshold",
name = "Confirmation Threshold",
description = "How many modified instances to be considered a large change",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
visible = Settings:getBinding("confirmationBehavior"):map(function(value)
return value == "Large Changes"
end),
input = e(TextInput, {
size = UDim2.new(0, 40, 0, 28),
text = Settings:getBinding("largeChangesConfirmationThreshold"):map(function(value)
return tostring(value)
end), end),
input = e(TextInput, {
size = UDim2.new(0, 40, 0, 28),
text = Settings:getBinding("largeChangesConfirmationThreshold"):map(function(value)
return tostring(value)
end),
transparency = self.props.transparency,
enabled = true,
onEntered = function(text)
local number = tonumber(string.match(text, "%d+"))
if number then
Settings:set("largeChangesConfirmationThreshold", math.clamp(number, 1, 999))
else
-- Force text back to last valid value
Settings:set(
"largeChangesConfirmationThreshold",
Settings:get("largeChangesConfirmationThreshold")
)
end
end,
}),
}),
PlaySounds = e(Setting, {
id = "playSounds",
name = "Play Sounds",
description = "Toggle sound effects",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = layoutIncrement(), enabled = true,
}), onEntered = function(text)
local number = tonumber(string.match(text, "%d+"))
CheckForUpdates = e(Setting, { if number then
id = "checkForUpdates", Settings:set("largeChangesConfirmationThreshold", math.clamp(number, 1, 999))
name = "Check For Updates", else
description = "Notify about newer compatible Rojo releases", -- Force text back to last valid value
transparency = self.props.transparency, Settings:set(
layoutOrder = layoutIncrement(), "largeChangesConfirmationThreshold",
}), Settings:get("largeChangesConfirmationThreshold")
)
CheckForPreleases = e(Setting, { end
id = "checkForPrereleases",
name = "Include Prerelease Updates",
description = "Include prereleases when checking for updates",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
visible = if string.find(debug.traceback(), "\n[^\n]-user_.-$") == nil
then false -- Must be a local install to allow prerelease checks
else Settings:getBinding("checkForUpdates"),
}),
AutoConnectPlaytestServer = e(Setting, {
id = "autoConnectPlaytestServer",
name = "Auto Connect Playtest Server",
description = "Automatically connect game server to Rojo when playtesting while connected in Edit",
tag = "unstable",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
OpenScriptsExternally = e(Setting, {
id = "openScriptsExternally",
name = "Open Scripts Externally",
description = "Attempt to open scripts in an external editor",
tag = "unstable",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
TwoWaySync = e(Setting, {
id = "twoWaySync",
name = "Two-Way Sync",
description = "Editing files in Studio will sync them into the filesystem",
locked = self.props.syncActive,
tag = "unstable",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
LogLevel = e(Setting, {
id = "logLevel",
name = "Log Level",
description = "Plugin output verbosity level",
tag = "debug",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
options = invertedLevels,
showReset = Settings:getBinding("logLevel"):map(function(value)
return value ~= "Info"
end),
onReset = function()
Settings:set("logLevel", "Info")
end, end,
}), }),
TypecheckingEnabled = e(Setting, {
id = "typecheckingEnabled",
name = "Typechecking",
description = "Toggle typechecking on the API surface",
tag = "debug",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
TimingLogsEnabled = e(Setting, {
id = "timingLogsEnabled",
name = "Timing Logs",
description = "Toggle logging timing of internal actions for benchmarking Rojo performance",
tag = "debug",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
[Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize)
end,
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
}),
}), }),
})
end) PlaySounds = e(Setting, {
id = "playSounds",
name = "Play Sounds",
description = "Toggle sound effects",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
EnableSyncFallback = e(Setting, {
id = "enableSyncFallback",
name = "Enable Sync Fallback",
description = "Whether Instances that fail to sync are remade as a fallback. If this is enabled, Instances may be destroyed and remade when syncing.",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
CheckForUpdates = e(Setting, {
id = "checkForUpdates",
name = "Check For Updates",
description = "Notify about newer compatible Rojo releases",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
CheckForPreleases = e(Setting, {
id = "checkForPrereleases",
name = "Include Prerelease Updates",
description = "Include prereleases when checking for updates",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
visible = if string.find(debug.traceback(), "\n[^\n]-user_.-$") == nil
then false -- Must be a local install to allow prerelease checks
else Settings:getBinding("checkForUpdates"),
}),
AutoConnectPlaytestServer = e(Setting, {
id = "autoConnectPlaytestServer",
name = "Auto Connect Playtest Server",
description = "Automatically connect game server to Rojo when playtesting while connected in Edit",
tag = "unstable",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
OpenScriptsExternally = e(Setting, {
id = "openScriptsExternally",
name = "Open Scripts Externally",
description = "Attempt to open scripts in an external editor",
tag = "unstable",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
TwoWaySync = e(Setting, {
id = "twoWaySync",
name = "Two-Way Sync",
description = "Editing files in Studio will sync them into the filesystem",
locked = self.props.syncActive,
lockedTooltip = "(Cannot change while currently syncing. Disconnect first.)",
tag = "unstable",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
LogLevel = e(Setting, {
id = "logLevel",
name = "Log Level",
description = "Plugin output verbosity level",
tag = "debug",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
options = invertedLevels,
showReset = Settings:getBinding("logLevel"):map(function(value)
return value ~= "Info"
end),
onReset = function()
Settings:set("logLevel", "Info")
end,
}),
TypecheckingEnabled = e(Setting, {
id = "typecheckingEnabled",
name = "Typechecking",
description = "Toggle typechecking on the API surface",
tag = "debug",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
TimingLogsEnabled = e(Setting, {
id = "timingLogsEnabled",
name = "Timing Logs",
description = "Toggle logging timing of internal actions for benchmarking Rojo performance",
tag = "debug",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
[Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize)
end,
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
}),
}),
})
end end
return SettingsPage return SettingsPage

View File

@@ -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 = {
-- Studio doesn't have good colors since their diffs use backgrounds, not text -- Very bright different colors in case some places were not updated to use
Add = if isDark then Color3.fromRGB(143, 227, 154) else Color3.fromRGB(41, 164, 45), -- the new background diff colors.
Remove = if isDark then Color3.fromRGB(242, 125, 125) else Color3.fromRGB(150, 29, 29), Add = Color3.fromRGB(255, 0, 255),
Edit = if isDark then Color3.fromRGB(120, 154, 248) else Color3.fromRGB(0, 70, 160), Remove = Color3.fromRGB(255, 0, 255),
Edit = Color3.fromRGB(255, 0, 255),
Row = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), Row = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
Warning = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText), Warning = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText),
Background = {
-- Studio doesn't have good colors since their diffs use backgrounds, not text
Add = if isDark then Color3.fromRGB(143, 227, 154) else Color3.fromRGB(41, 164, 45),
Remove = if isDark then Color3.fromRGB(242, 125, 125) else Color3.fromRGB(150, 29, 29),
Edit = if isDark then Color3.fromRGB(120, 154, 248) else Color3.fromRGB(0, 70, 160),
Remain = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
},
Text = {
Add = if isDark then Color3.new(0, 0, 0) else Color3.new(1, 1, 1),
Remove = if isDark then Color3.new(0, 0, 0) else Color3.new(1, 1, 1),
Edit = if isDark then Color3.new(0, 0, 0) else Color3.new(1, 1, 1),
Remain = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
},
}, },
ConnectionDetails = { 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()

View 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

View File

@@ -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, { end
Connect = { end)
text = "Connect",
style = "Solid", self:tryAutoReconnect():andThen(function(didReconnect)
layoutOrder = 1, if not didReconnect then
onClick = function(notification) self:checkSyncReminder()
notification:dismiss() end
self:startSession() end)
end,
},
Dismiss = {
text = "Dismiss",
style = "Bordered",
layoutOrder = 2,
onClick = function(notification)
notification:dismiss()
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)
notifications[id] = nil local notifications = table.clone(prevState.notifications)
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 Dismiss = {
return text = "Dismiss",
end style = "Bordered",
layoutOrder = 2,
self:addNotification( },
string.format(
"A newer compatible version of Rojo, %s, was published %s! Go to the Rojo releases page to learn more.",
Version.display(latestCompatibleVersion.version),
timeUtil.elapsedToText(DateTime.now().UnixTimestamp - latestCompatibleVersion.publishedUnixTimestamp)
),
500,
{
Dismiss = {
text = "Dismiss",
style = "Bordered",
layoutOrder = 2,
onClick = function(notification)
notification:dismiss()
end,
}, },
} })
) 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)

View File

@@ -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.

View File

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

View File

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

View File

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

View File

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

View File

@@ -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")

View File

@@ -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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
"ReplicatedStorage": { "ReplicatedStorage": {
"Rojo": { "Rojo": {
"$path": "default.project.json" "$path": "../plugin.project.json"
}, },
"Packages": { "Packages": {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: ~

View File

@@ -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: ~

View File

@@ -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: ~

View File

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

View File

@@ -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: ~

View File

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

View File

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

View File

@@ -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: ~

View File

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

View File

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

View File

@@ -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: ~

View File

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

View File

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

View File

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

View File

@@ -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: ~

View File

@@ -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: ~

View File

@@ -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: ~

View File

@@ -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: ~

View File

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

View File

@@ -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: ~

View File

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

Some files were not shown because too many files have changed in this diff Show More