Compare commits

..

241 Commits

Author SHA1 Message Date
8053909bd0 Add --git-since option to rojo serve
- Add new GitFilter struct for tracking files changed since a Git reference
- Only sync changed (added/deleted/modified) files to Roblox Studio
- Files remain acknowledged once synced, even if content is reverted
- Add enhanced logging for debugging sync issues
- Force acknowledge project structure to prevent 'Cannot sync a model as a place' errors
2026-01-19 22:02:59 +01:00
Micah
d08780fc14 Ensure that pruned Instances aren't treated as existing in syncback (#1179)
Closes #1178.
2025-11-29 21:21:48 -08:00
Micah
b89cc7f398 Release memofs v0.3.1 (#1175) 2025-11-27 12:32:57 -08:00
Micah
42568b9709 Release Rojo v7.7.0-rc.1 (#1174) 2025-11-27 12:10:57 -08:00
boatbomber
87f58e0a55 Use WebSocket instead of Long Polling (#1142) 2025-11-26 19:57:01 -08:00
Micah
a61a1bef55 Roundtrip schemas in syncback (#1173) 2025-11-26 16:11:39 -08:00
Micah
a99e877b7c Actually skip .gitignore if --skip-git is passed to init (#1172) 2025-11-26 13:59:12 -08:00
Ken Loeffler
93e9c51204 Fix rojo plugin install by adding Vfs::exists (#1169) 2025-11-21 07:04:34 -08:00
Ken Loeffler
015b5bda14 Set crate and plugin versions to 7.7.0-prealpha (#1170) 2025-11-21 07:02:09 -08:00
Micah
2b47861a4f Properly support EnumItem variants in hashing and variant_eq (#1165) 2025-11-19 19:18:14 -08:00
Micah
9b5a07191b Implement Syncback to support converting Roblox files to a Rojo project (#937)
This is a very large commit.
Consider checking the linked PR for more information.
2025-11-19 09:21:33 -08:00
boatbomber
071b6e7e23 Improved string diff viewer (#994) 2025-11-18 20:26:44 -08:00
quaywinn
31ec216a95 Remove pairs() and ipairs() (#1150) 2025-11-18 18:49:52 -08:00
Micah
ea70d89291 Support .jsonc extension for all JSON files (#1159) 2025-11-18 18:47:43 -08:00
quaywinn
03410ced6d Use buffer for ClassIcon EditableImages (#1149) 2025-11-07 13:07:19 -08:00
Micah
825726c883 Release 7.6.1 (#1151) 2025-11-06 18:49:05 -08:00
boatbomber
54e63d88d4 Slightly improve initial sync hangs (#1140) 2025-11-06 00:06:42 -08:00
boatbomber
4018c97cb6 Make CHANGELOG.md use consistent style (#1146) 2025-10-28 19:26:48 -07:00
boatbomber
d0b029f995 Add JSONC Support for Project, Meta, and Model JSON files (#1144)
Replaces `serde_json` parsing with `jsonc-parser` throughout the
codebase, enabling support for **comments** and **trailing commas** in
all JSON files including `.project.json`, `.model.json`, and
`.meta.json` files.
MSRV bumps from `1.83.0` to `1.88.0` in order to
use the jsonc_parser dependency.
2025-10-28 17:29:57 -07:00
Sebastian Stachowicz
aabe6d11b2 Update default gitignores to include sourcemap (#1145) 2025-10-28 17:28:55 -07:00
boatbomber
181cc37744 Improve sync fallback robustness (#1135) 2025-10-20 20:13:47 -07:00
boatbomber
cd78f5c02c Fix postcommit callbacks being skipped (#1132) 2025-10-14 12:13:59 -07:00
Micah
441c469966 Release Rojo v7.6.0 (#1125) 2025-10-10 19:17:55 -07:00
Micah
f3c423d77d Fix the various lints (#1124) 2025-10-10 13:00:56 -07:00
Micah
beb497878b Add flag for skipping git initialization to init command (#1122) 2025-10-07 17:12:22 -07:00
Micah
6ea95d487c Refactor init command (#1117) 2025-09-30 14:38:38 -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
Kenneth Loeffler
73097075d4 Update rbx-dom dependencies (#965) 2024-08-22 20:03:06 +01:00
Micah
5e1cab2e75 Actually include attribute-defined properties in patch computation (#944) 2024-08-19 15:41:02 -07:00
Micah
30f439caec Add 7.4.3 to changelog (#960)
After 7.4.3 released, I forgot to update the changelog on master. This
fixes that.
2024-08-15 16:42:11 +00:00
boatbomber
4b5db4e5a9 Check for compatible updates in plugin (#832) 2024-08-05 11:34:29 -07:00
Barış
3fa1d6b09c Set linguist language of lua files to luau (#956) 2024-08-02 10:03:57 -07:00
Micah
6051a5f1f1 Update Changelog to include 7.4.2 (#951) 2024-07-23 14:39:04 -07:00
Kenneth Loeffler
5f7dd45361 Sleep between file copies and serve for macOS serve tests (#945) 2024-07-20 09:52:05 -07:00
Micah
3ca975d81d Correct issue with default.project.json files with no name being named default after change (#917)
Co-authored-by: Kenneth Loeffler <kenloef@gmail.com>
2024-07-15 09:24:51 -07:00
Micah
7e2bab921a Support setting referent properties via attributes (#843)
Co-authored-by: Kenneth Loeffler <kenloef@gmail.com>
2024-06-20 23:48:52 +01:00
dependabot[bot]
a7b45ee859 Bump h2 from 0.3.24 to 0.3.26 (#921) 2024-05-30 12:45:38 -07:00
boatbomber
62f4a1f3c2 Use history recording and don't do anything permanent (#915) 2024-05-30 12:28:58 -07:00
boatbomber
3d4e387d35 Redesign settings UI in plugin (#886) 2024-05-13 10:36:03 -07:00
Micah
2c46640105 Allow openScriptsExternally option to be changed during sync (#911) 2024-05-08 12:34:00 -07:00
dependabot[bot]
41443d3989 Bump rustls from 0.21.10 to 0.21.11 (#905) 2024-04-19 20:03:55 +00:00
Kenneth Loeffler
4b3470d30b Fix removing trailing newlines by using str::replace in memofs (#903) 2024-04-17 11:55:23 -07:00
Kenneth Loeffler
ce71a3df4d Release workflow maintenance (#902) 2024-04-17 11:55:08 -07:00
Kenneth Loeffler
7232721b87 Use dtolnay/rust-toolchain and upgrade to checkout v4 in CI workflow (#900)
This PR performs some routine maintenance on our CI workflow:

* Replaces `actions-rs/toolchain` with `dtolnay/rust-toolchain`. The
actions at `actions-rs` are no longer maintained, and they use
deprecated GitHub Actions APIs. dtolnay's action does not support the
`override` option, but we didn't actually need to use it anyway.
* Upgrades `actions/checkout` to v4, because v3 causes some warnings
since it uses Node.js 16, which is deprecated.
2024-04-09 14:55:42 -07:00
boatbomber
b2f133e6f1 Patch visualizer redesign (#883) 2024-04-02 00:04:58 -07:00
Kenneth Loeffler
87920964d7 Release memofs 0.3.0, bump Rojo dependency (#894) 2024-03-25 10:48:27 -07:00
Barış
c7a4f892e3 Add never option to Confirmation (#893) 2024-03-14 19:41:21 +00:00
EgoMoose
8f9e307930 Trim plugin version string (#890)
Duplicate of https://github.com/rojo-rbx/rojo/pull/889, but based on
master as per request.

This PR is a very small change that fixes the string pattern that reads
the rojo version from `Version.txt`. Currently this reads an extra
new-line character which makes reading the version text in the plugin
difficult.

It seems the rust side of things already trims the string when
comparing, but the lua side does not.
2024-03-13 09:50:41 -07:00
Micah
856d43ce69 Update Cargo dependencies (#887) 2024-03-04 15:20:58 -08:00
boatbomber
26181a5a1f Use GuiState instead of manual calculation for tooltips (#884) 2024-02-29 14:50:06 -08:00
boatbomber
edf87bf9a3 Build tree ancestry correctly (#882) 2024-02-29 14:27:46 -08:00
Kenneth Loeffler
5f51538e0b Update master's changelog in preparation for 7.4.1 release (#873)
This PR edits the changelog on master to reflect 7.4.1's release
2024-02-21 01:46:01 +00:00
Micah
48bb760739 Make the name field in projects optional (#870)
Closes #858.

If a project is named `default.project.json`, it acts as an `init` file
and gains the name of the folder it's inside of. If it is named
something other than `default.project.json`, it gains the name of the
file with `.project.json` trimmed off. So e.g. `foo.project.json`
becomes `foo`.
2024-02-20 17:25:57 -08:00
Micah
42121a9fc9 Allow building Rojo with profile-with-tracy feature (#862) 2024-02-20 14:56:55 -08:00
Filip Tibell
02d79a4749 Migrate to using Rustls (#861) 2024-02-20 14:56:31 -08:00
Kenneth Loeffler
ddb26c73bd rbx_dom_lua rojo-rbx/rbx-dom@6ccd30f (custom pivot get/set) (#868) 2024-02-20 12:08:55 -08:00
boatbomber
8ff064fe28 Add benchmarking, perf gains, and better settings UI (#850) 2024-02-12 15:58:35 -08:00
Kenneth Loeffler
cf25eb0833 Normalize line endings to LF in Lua middleware (#854) 2024-02-12 14:58:03 -08:00
boatbomber
5c4260f3ac Catch failed http requests that didn't error so we can handle them correctly (#847) 2024-02-01 21:29:36 +00:00
Kenneth Loeffler
7abf19804c Ignore any unreadable property in Reconciler:diff (#848) 2024-02-01 21:05:44 +00:00
boatbomber
df707d5bef Lint plugin src (#846) 2024-01-31 21:08:07 -08:00
boatbomber
f3b0b0027e Catch more sync failures (#845)
- Catch removal failures
- Catch name change failures
- Don't remove IDs for instances if they weren't actually destroyed
2024-01-31 17:07:01 -08:00
boatbomber
106a01223e Show failed additions and removals in visualizer (#844) 2024-01-31 14:45:28 -08:00
boatbomber
506a60d0be Play Solo & Test Server auto connect (#840)
When enabled, the `baseurl` of the session is written to
`workspace:SetAttribute("__Rojo_ConnectionUrl")` so that the test server
can connect to that session automatically.

This works for Play Solo and Local Test Server. It is marked
experimental for now (and disabled by default) since connecting during a
playtest session is... not polished. Rojo may overwrite things and cause
headaches. Further work can be done later.
2024-01-30 12:51:45 -08:00
boatbomber
4018607b77 Visualize table changes (#834)
Implements a pop out diff view for table properties like Attributes and
Tags
2024-01-22 12:26:41 -08:00
boatbomber
1cc720ad34 Use Studio theming (#838)
Updates our Theme provider to use Studio colors. A few components look
ever so slightly different now, but more in line with Studio.
2024-01-22 11:22:21 -08:00
Micah
73828af715 Add a new syncRules field project files to allow users to specify middleware to use for files (#813) 2024-01-19 22:18:17 -08:00
Micah
c0a96e3811 Release v7.4.0 (#837) 2024-01-16 12:12:40 -08:00
Micah
9d0d76f0a5 Ensure plugin and Cargo version match exact at compile-time (#836) 2024-01-16 14:09:10 -06:00
Micah
c7173ac832 Don't serialize emitLegacyScripts if it's None (#835) 2024-01-16 10:09:16 -08:00
boatbomber
b12ce47e7e Don't remind to sync if the lock is claimed (#833)
If the sync lock is claimed in Team Create, the user cannot sync.
Therefore, a sync reminder notification is unhelpful as it is calling to
an invalid action.
2024-01-12 12:35:29 -08:00
Barış
269272983b Changed file extensions of init command from lua to luau (#831) 2024-01-05 16:00:49 -08:00
Kenneth Loeffler
6adc5eb9fb Conserve CI minutes via cache, skip macOS+Windows MSRV builds (#827)
Windows and macOS runners consume GitHub Actions minutes at [2x and 10x
the rate of Linux runners,
respectively](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#minute-multipliers).
This is a bit concerning now that there are two Windows jobs and two
macOS jobs, introduced in #825.

This PR aims to reduce the cost by:
* Adding [rust-cache](https://github.com/Swatinem/rust-cache/) to reduce
the amount of time spent. I'm aware there were some concerns raised
about CI caches in general in #496 - are they still a blocker?
* Removing the unnecessary Windows and macOS MSRV build jobs. If an MSRV
build fails on one platform due to usage of new language features, then
it will fail on all of them.

@Kampfkarren may have to change this repository's required status checks
before this PR can be merged
2024-01-02 15:49:24 -08:00
boatbomber
fd8bc8ae3f Improve visualization for arrays (#829) 2024-01-02 17:32:37 +00:00
Kenneth Loeffler
3369b0d429 Downgrade to Notify 4, use FSEvents, use minimal recursive watches (#830) 2024-01-02 09:26:06 -08:00
Kenneth Loeffler
097d39e8ce Fix move_folders_of_stuff (#826)
This is a fairly important test verifying whether the action of moving a
folder into a watched folder is correctly detected and processed. It was
disabled in
b43b45be8f.
The fact that it failed indicates a possible bug in change processing,
so in this PR, I'll re-enable the test, investigate why it fails, and
fix it.
2023-12-31 12:02:54 -08:00
Kenneth Loeffler
11fa08e6d6 Run CI workflow on Windows and macOS (#825)
This PR adds macOS and Windows jobs to the CI workflow. This allows us
to see when changes break functionality on any supported platform, which
is particularly important for changes that involve the file system or
file watcher.
2023-12-29 17:46:58 -08:00
Kenneth Loeffler
96987af71d Fix broken serve tests on macOS (#824)
Right now, serve tests will fail when Rojo is built with the FSEvent
backend. The cause is essentially due to the fact that `/var` (where
temporary directories for serve tests are located) on macOS is actually
a symlink to `/private/var`. Paths coming from FSEvent always have
symlinks expanded, but Rojo never expands symlinks. So, Rojo's paths
during these tests look like `/var/*` while the FSEvent paths look like
`/private/var/*`. When Rojo's change processor receives these events, it
considers them outside the project and does not apply any changes,
causing serve tests to time out.

To work around this, we can call `Path::canonicalize` before passing the
project path to `rojo serve` during serve tests. Rojo does need to
better support symlinks (which would also solve the problem), but I
think that can be left for another day because it's larger in scope and
I mostly just want working tests before addressing #609.
2023-12-28 23:17:00 +00:00
Vee
23327cb3ef Fix preloading assets in plugin (#819)
`gatherAssetUrlsRecursive` now returns asset URLs deeper than one layer.
2023-12-04 16:02:25 +00:00
Jack T
b43b45be8f Upgrade to Notify 6 (#816) 2023-11-23 16:16:43 -08:00
Micah
41994ec82e Release v7.4.0-rc3 (#811) 2023-10-25 17:01:06 -07:00
Micah
cd14ea7c62 Remove unnecessary borrows (#810) 2023-10-25 17:59:02 -05:00
Kenneth Loeffler
9f13bca6b8 rbx_dom_lua rojo-rbx/rbx-dom@440f3723 (attribute validation) (#809)
Brings over some changes to rbx_dom_lua to validate attribute names
before calling `Instance:SetAttribute`. This should prevent Rojo from
falling over when it attempts to sync an attribute with an invalid name.
2023-10-25 14:21:58 -07:00
Kenneth Loeffler
f4252c3e97 Update changelog in preparation for 7.4.0-rc3 (#808)
Summarizes recent changes since 7.4.0-rc2 in the changelog
2023-10-23 22:54:48 +00:00
Kenneth Loeffler
6598867d3d Bump rbx_binary to 0.7.3 (#807)
Bumps rbx_binary's version to 0.7.3 to bring in the missing
`SecurityCapabilities` serialization fallback default
2023-10-23 22:43:00 +00:00
dependabot[bot]
f39e040a0d Bump rustix from 0.38.15 to 0.38.20 (#806) 2023-10-23 22:38:28 +00:00
Kenneth Loeffler
a3d140269b Demote unapplied patch warnings to debug logs (#805)
These warnings always appear for properties like `Capabilities`,
`SourceAssetId`, etc. and tend to scare users who are syncing models.
This information is now surfaced in the patch visualizer, so I think
these warnings can be demoted to debug logs.
2023-10-23 11:47:50 -07:00
Kenneth Loeffler
feac29ea40 Fix PatchTree incorrect changeList entries on decode failure (#804) 2023-10-22 16:58:12 -07:00
Kenneth Loeffler
834c8cdbca rbx_dom_lua rojo-rbx/rbx-dom@0e10232b (SecurityCapabilities) (#803)
Closes #802.
2023-10-22 16:57:59 -07:00
Kenneth Loeffler
d441fbdf91 Bump rbx_dom_lua rojo-rbx/rbx-dom@e7a5b91c (ScriptEditorService) (#801) 2023-10-17 14:15:47 -07:00
Filip Tibell
e897f524dc Skip sourcemap generation when unaffected by changes in watch mode (#800) 2023-10-13 08:38:21 -07:00
Micah
1caf9446d8 Rojo 7.4.0-rc2 (#798) 2023-10-04 05:36:57 +00:00
Micah
bfd2c885db Properly handle build metadata in semver parsing in plugin (#797) 2023-10-04 05:23:18 +00:00
Kampfkarren
f467fa4e59 7.4.0-rc.1 [release] 2023-10-03 21:07:06 -07:00
Micah
41fca4a2bb Set version to Rojo 7.4.0-rc1 (#796)
Due to the rewrite of the plugin's core sync loop and the change for the
Notify backend on MacOS, along with all the other changes in 7.4.0, it
makes sense for us to use a release candidate before actually cutting a
proper `7.4.0` release.
2023-10-04 00:04:21 -04:00
Micah
d38f955144 Update rbx_dom dependencies (#795)
In preparation for a new release, rbx_dom needs to be updated.
2023-10-03 16:32:22 -07:00
Micah
010e50a25d Add check to ensure plugin version matches cargo version (#794)
This modifies Rojo's build script to throw a fit if we're building a
plugin with a semver incompatible version. In the process, it moves the
version of the plugin to a file named `Version.txt` that's parsed at
runtime. This should be minimally invasive but it's technically worse
for performance than the hardcoded table and string we had before.

This feels better than a CI check or just manually verifying because it
makes it physically impossible for us to forget since Rojo won't build
with it being wrong.
2023-10-03 10:29:47 -04:00
Micah
eab7c607cd Don't include package author in CLI help (#793)
LPG is at the moment listed by name and email in the `help` for Rojo. We
should probably remove that before cutting a new release.
2023-10-03 10:22:47 -04:00
Kenneth Loeffler
3cafbf7f1a Use PollWatcher in memofs StdBackend on macOS (#783) 2023-09-26 15:41:33 -07:00
Kenneth Loeffler
d7277b5a5b Clean up changelog in preparation for Rojo 7.4.0 (#788)
This PR aims to clean up the changelog in preparation for 7.4.0. We
should focus on making changes clear to users with examples, removing
any entries that they won't care about, and consolidating tightly
related changes

---------

Co-authored-by: boatbomber <zack@boatbomber.com>
Co-authored-by: Micah <48431591+nezuo@users.noreply.github.com>
2023-09-25 16:13:01 -07:00
Sasial
bb8dd1402d Add RunContext support for script outputs (#765)
Resolves #667

This PR:

- Introduces a new field in the project file: `scriptType` which has the
default value of `Class` (in parity with previous versions), but can
also be `RunContext`.
- This is then passed to `InstanceContext` from the `Project` struct.
- This then changes the RunContext in the lua `snapshot_middleware`

---------

Co-authored-by: Micah <dekkonot@rocketmail.com>
2023-09-23 13:28:09 -07:00
Barış
539cd0d418 Create .git-blame-ignore-revs & Add Stylua pr (#787)
adds stylua pr to ignore so it doesn't show on git blame history
2023-09-19 05:56:39 +00:00
boatbomber
0f8e1625d5 Stylua formatting (#785)
Uses Stylua to format all existing Lua files, and adds a CI check in
`lint` to pin this improvement. Excludes formatting dependencies, of
course.
2023-09-18 15:39:46 -07:00
boatbomber
840e9bedb2 Use GUIDs for ephemeral PluginWidgets (#784)
Because Roact will destroy and recreate a plugin widget if it unmounts
and remounts, Studio will complain about making a new widget with the
same ID as the old one.

The simplest solution is to just use GUIDs so we never have to worry
about this again. The ID is used internally for storing the widget's
dock state and other internal details, so we don't want *all* our
widgets to use GUIDs, only the ephemeral popup ones.
2023-09-18 21:44:14 +00:00
Micah
e11ad476fc Use conflicts_with for build flags (#779) 2023-09-11 13:32:43 -07:00
boatbomber
c43726bc75 Confirmation behaviors (#774) 2023-08-20 12:37:40 -07:00
Kenneth Loeffler
c9ab933a23 Update rbx_dom_lua to rojo-rbx/rbx-dom@7b03132 (#775)
Fixes a problem where the MaterialColors Lua encoder would return nothing
2023-08-16 16:47:04 -07:00
Micah
066a0b1668 Allow Terrain to be included in projects without a classname (#771)
Services, `StarterPlayerScripts`, and `StarterCharacterScripts` are
currently special-cased to allow them to be specified in project files
without a classname. This does the same to `Terrain` since it's a
singleton in the same style as those.
2023-08-15 05:41:49 -07:00
Micah
aa68fe412e Add ambiguous value support for MaterialColors (#770)
The last release of rbx_dom had support for `Terrain.MaterialColors`.
This allows it to be specified directly instead of only via the
fully-qualified syntax.
2023-08-14 15:41:15 -07:00
Micah
d748ea7e40 Update rbx_dom dependencies (#768) 2023-08-09 21:54:47 -07:00
boatbomber
a7a282078f Update Highlighter submodule from 0.8.1 to 0.8.2 (#767)
Highlighter had an edge case with changed events racing, which has been
resolved in the 0.8.2 patch.
2023-08-09 06:40:35 +00:00
boatbomber
2fad3b588a Fix theme (#763) 2023-08-03 00:47:03 +00:00
utrain
4cb5d4a9c5 Update Theme to use Color3.fromHex (#761) 2023-08-01 09:36:29 -07:00
Micah
5b22ef192e Don't override initial state for source diff view (#760) 2023-08-01 09:33:06 -07:00
boatbomber
34024d8524 Fix PatchTree performance issues (#755)
When building the tree, I've implemented a few improvements:

- We no longer traverse the full ancestry for every leaf node- we exit
early when we find a node that already exists
- We no longer search the entire tree to see if a node id exists before
creating one with that id, we just check if is in the map
2023-07-31 22:26:16 -07:00
boatbomber
ecc31dea15 View rich diffs for Source property changes (#748) 2023-07-26 23:50:29 -07:00
utrain
d0e48d9bdc Add some logging/error-handling for opener errors! (#745)
After seeing an issue with the opener not opening, it was strange to see
that there was no logging or error handling! This PR aims to solve that.
2023-07-26 15:10:02 -07:00
Micah
f6fc5599c0 Add plugin template (#738)
Adds a new plugin template to the `init` command: `rojo init --kind
plugin`.
2023-07-23 17:12:48 -07:00
Micah
89b6666436 Support ambiguous syntax for Font properties (#731) 2023-07-20 21:30:18 -07:00
Micah
94d45a2262 Remove references to LPG's patreon (#744)
Given that LPG is not maintaining Rojo or its related projects, it does
not make sense for us to suggest his patreon. This PR removes them.
2023-07-20 07:57:02 -07:00
Micah
dc17a185ca Add plugin build flag (#735)
Resolves #715.
2023-07-17 19:15:40 -07:00
boatbomber
4915477823 Expose reconciler hooks on servesession (#741) 2023-07-17 04:03:06 +00:00
boatbomber
8662d2227c Improve Rojo sync reliability (#739)
Uses non-recursive main loop and avoids hanging promises
2023-07-16 14:59:30 -07:00
dependabot[bot]
dd01a9bef3 Bump openssl from 0.10.51 to 0.10.55 (#737) 2023-07-14 14:04:11 -07:00
Shae
6e320b1fd5 Add support for TOML files (#633)
TOML maps well to Lua, is easier to read and write than JSON, and is
commonly used by Roblox tools.

Use cases:
* Put game, plugin, or library config in a toml file
* Sync in toml files generated by tools
* Sync in config files for tools so that the game can double-check that
the config file has been followed. (e.g. check that packages match
versions specified in wally.toml)
2023-07-14 20:36:50 +00:00
boatbomber
6e40993199 Rework patch visualizer with many fixes and improvements (#726)
- Reconciler now has precommit and postcommit hooks for patch applying
  - This is used to compute a patch tree snapshot precommit and update the
tree metadata postcommit
- PatchVisualizer can now display Removes that happened during sync
  - It was previously missing because the removed objects no longer
existed so it couldn't get any info on them (This is resolved because
the info is gotten in precommit, before the instance was removed)
- PatchVisualizer now shows Old and New values instead of just Incoming
during sync
  - (Still displays Current and Incoming during confirmation)
  - This is much more useful, since you now see what the changes were and
not just which things were changed
- PatchVisualizer displays clarifying message when initial sync has no
changes instead of just showing a blank box
- Objects in the tree UI no longer get stuck expanded when the next
patch has the same instance but different info on it
- Objects in the tree UI correctly become selectable after their
instance is added and unclickable when removed during sync
2023-07-13 20:09:19 -07:00
boatbomber
9d48af2b50 Better settings control (#725)
- Lock some settings during sync
- Make experimental features more clearly labelled
2023-07-12 13:00:07 -07:00
boatbomber
28d48a76e3 Fix tooltips crashing on thread cancellation edge cases (#727) 2023-07-12 10:01:21 -07:00
utrain
80eb14f9da Removed InstanceSnapshot snapshot_id's redudant Ref. (#730)
Ref now is an optional inside, so it's redundant to have an option
wrapping an option. The only snapshots that were changed were any that
had a Ref within (from none to zeroed). Some also had some newlines
added in the end.
2023-07-12 10:00:09 -07:00
boatbomber
623fa06d52 Improve tooltip behavior (#723) 2023-07-08 21:41:47 -07:00
boatbomber
7154113c13 Add buttons on connected page (#722) 2023-07-08 21:23:25 -07:00
boatbomber
0a932ff880 Show failed to apply in visualizer (#717)
Objects with failed changes will highlight, and the specific failed
changes in their list will highlight as well.
2023-07-08 19:31:26 -07:00
boatbomber
7ef4a1ff12 Select Instances from diff tree (#709) 2023-07-08 18:45:25 -07:00
Barış
ccc52b69d2 (fix) Move changelog job to it's own workflow (#720)
- moved the job to its own workflow file
- changed the label to `skip changelog`
2023-07-09 01:30:59 +00:00
Barış
8139fdc738 (feat) Add Changelog Check Action (#719) 2023-07-09 00:44:28 +00:00
boatbomber
a4fd53d516 Update changelog to include #713 2023-07-08 15:54:19 -07:00
boatbomber
27357110b5 Handle strings and instances in patch removes for visualizer (#713)
When an object is deleted in a patch, it is either represented with an
ID or an Instance. On initial sync, removals are instances since the map
does not contain those instances. Later removals of managed objects use
an ID. The patch visualizer only handled instances, so this fixes that.

Closes #710.
2023-07-08 14:37:42 -07:00
boatbomber
fde78738b6 Improve sync info (#692)
- Fixed an edge case where disconnecting and then reconnecting would
retain outdated info
- Fixed an issue where new patches wouldn't immediately update the
change info text
- Removed extraneous changes count info, as it was not useful and could
be checked in the visualizer anyway
- Added warning info for when some changes fail to apply
- Updates timestamp of last sync even if patch was empty, to have a more
accurate signal on Rojo's uptime
2023-07-05 21:38:11 +00:00
boatbomber
ce530e795a Fix Rojo breaking when users undo/redo in Studio (#708) 2023-07-05 14:09:11 -07:00
boatbomber
658d211779 Sync reminder notification & notification actions (#689)
Implements and closes #652.

---------

Co-authored-by: Chris Chang <51393127+chriscerie@users.noreply.github.com>
Co-authored-by: Micah <git@dekkonot.com>
2023-07-04 20:09:41 +00:00
boatbomber
66c1cd0d93 Add protection against syncing non place projects (#691)
Closes #113.

Does what it says on the tin. When you try to sync a non-place project,
it gives a clear message instead of crashing.
2023-06-30 13:44:42 -07:00
boatbomber
55ac231cec Skip confirming patches that only contain a datamodel name change (#688)
Closes #672.

Skips the user confirmation if the patch contains only a datamodel name
change.

I decided to build generic PatchSet utility functions in case we need to
use similar logic in the future, in addition to maintaining clear
division of duties. The app code shouldn't be too dependent upon patch
internal structure when we can avoid it.

---------

Co-authored-by: Kenneth Loeffler <kenloef@gmail.com>
2023-06-30 11:21:36 -07:00
Kenneth Loeffler
67674d53a2 Fix remaining clippy lints (#687)
The eighth (and final) in a series of PRs that aim to get CI passing
2023-06-30 11:06:43 -07:00
Kenneth Loeffler
8646b2dfce Fix snapshot middleware clippy lints (#686)
The seventh in a series of PRs that aim to get CI passing
2023-06-30 11:06:12 -07:00
Kenneth Loeffler
a2f68c2e3c Fix snapshot clippy lints (#685)
The sixth in a series of PRs that aim to get CI passing
2023-06-30 11:05:55 -07:00
Kenneth Loeffler
5b1a090c5e Fix serve session clippy lints (#684)
The fifth in a series of PRs that aim to get CI passing
2023-06-30 11:05:30 -07:00
Kenneth Loeffler
e9efa238b0 Fix project clippy lints (#683)
The fourth in a series of PRs that aim to get CI passing
2023-06-30 11:04:56 -07:00
Kenneth Loeffler
0dabd8a1f6 Fix memofs clippy lints (#682)
The third in a series of PRs that aim to get CI passing
2023-06-30 11:04:04 -07:00
Kenneth Loeffler
b7a1f82f56 Fix cli clippy lints (#681)
The second in a series of PRs that aim to get CI passing
2023-06-30 11:03:37 -07:00
Kenneth Loeffler
2507e096b7 Fix change processor clippy lints (#680)
Bear with me here...

The first in a series of PRs that aim to get CI passing
2023-06-30 11:03:09 -07:00
boatbomber
b303b0a99c Add PRs 674 and 675 to changelog (#693)
Forgot to update changelog in those PRs, this remedies that
2023-06-30 11:02:14 -07:00
Filip Tibell
342fb57d14 Fix compilation target in release workflow (#701)
Fixes macOS aarch64 builds not actually outputting x86 binaries by
specifying the `--target` flag during compilation in the release
workflow. These are the same changes as [this PR in
Aftman](https://github.com/LPGhatguy/aftman/pull/34), which had the same
issue, and uses the same workflow.
2023-06-30 10:53:19 -07:00
Boegie19
a9ca77e27f Change gitmodules from ssh to https (#696)
Why if you want to use ssh you need todo more setup aka add a ssh key
into github while with https you don't have to do the extra work aka
makes rojo easier to contribute to.
2023-06-16 11:41:30 -07:00
boatbomber
6542304340 Fix the diff visualizer of connected sessions (#674)
Clicking on the "X changes X ago" message opens up a handy diff
visualizer to see what those changes were. However, it had quite a few
issues that needed fixing.

- Disconnecting a session with it expanded caused an error as it tried
to read the serveSession that no longer exists during the page fade
transition. (#671)
- Resolved by converting to stateful component and holding the
serveSession during the lifetime to ensure it can render the last known
changes during the fade transition
- Leaving it open while new changes are synced did not update the
visualizer
- The patch data was piggybacking on an existing binding, which meant
that new patches did not trigger rerender.
    - Resolved by converting to state
    - Also made some improvements to that old binding
- Moved from app to connected page for better organization and
separation of duties
      - No more useless updates causing rerenders with no real change
- Scroll window child component wouldn't actually display the updated
visuals
    - Resolved by making major improvements to VirtualScroller
      - Made more robust against edge case states
      - Made smarter about knowing when it needs to refresh

As you can see in this slow motion GIF, it works now.

![slowedDemo](https://github.com/rojo-rbx/rojo/assets/40185666/c9fc8489-72a9-47be-ae45-9c420e1535d4)
2023-06-03 22:46:16 -07:00
boatbomber
6b0f7f94b6 Fix disconnected session activity (#675)
When a session is disconnected, the apiContext long-polling for messages
continues until resolved/rejected. This means that even after a session
is disconnected, a message can be received and handled.

This leads to bad behavior, as the session was already cleaned up and
the message cannot be handled correctly. The instance map was cleaned up
upon disconnect, so it will warn about unapplied changes to unknown
instances. (Like #512)

It's very easy to repro:
Connect a session, disconnect it, then save a change.


https://github.com/rojo-rbx/rojo/assets/40185666/846a7269-7043-4727-9f9c-b3ac55a18a3a

-----------

This PR fixes that neatly by tracking all active requests in a map, and
cancelling their promises when we disconnect.
2023-06-01 08:18:04 -07:00
Kenneth Loeffler
d87c76a23e Switch plugin packages back to git submodules (#678)
Alright, so I hate to be the one to do this, but #584 broke crates.io
publishing and also caused librojo to be unusable. I see that there was
some discussion on Discord shortly after the problem was realized, but
there was no action taken.

I think keeping librojo and publishing working far, far outweigh any
convenience added by Wally.

I've kept the same `Packages` naming convention to keep the diff
minimal.
2023-05-26 10:26:21 -07:00
Filip Tibell
305423b856 Sourcemap performance improvements (#668)
This PR brings two performance improvements to the `rojo sourcemap`
command:

- Use `rayon` with a small threadpool to parallelize sourcemap
generation while still keeping startup cost very low
- Remove conversions to owned strings and use lifetimes tied to the dom
instead, which mostly improves performance with the
`--include-non-scripts` flag enabled

From my personal testing on an M1 mac this decreases the sourcemap
generation time of our games by 2x or more, from ~20ms to ~8ms on one
project and ~30ms to ~15ms on another. Generation is pretty fast to
begin with but since sourcemaps are heavily used in interactive tools
(like luau-lsp) a difference of a couple frames can be great for ux.
2023-05-06 01:01:15 -07:00
Miizzuu
4b62190aff Fix wrong version of Rojo displaying in studio. (#669) 2023-05-06 01:00:45 -07:00
Lucien Greathouse
e17771a6a5 Release v7.3.0 2023-04-22 16:07:39 -04:00
Lucien Greathouse
bac30ae78b Update MSRV to try to fix CI workflow 2023-04-22 15:58:14 -04:00
Lucien Greathouse
c0219922b2 Update dependencies 2023-04-22 15:44:49 -04:00
boatbomber
b5ed952d5c Add visual diffs to syncing (#603)
* Add user confirmation to initial sync

* Use "Accept" instead of "Confirm"

* Draw tree alphabetically for determinism

* Add diff table dropdown

* Add diff table to newly added objects

* Unblock keybind workflow

* Only show reject button when two way is enabled

* Try to patch back to the files when changes are rejected

* Improve text spacing of the prop diff table

* Skip user confirmation of perfect syncs

* Give instances names for debugging UI

* Optimize tree building

* Efficiency: dynamic virtual scrolling & lazy rendering

* Simplify virtual scroller logic and avoid wasteful rerenders

* Remove debug print

* Consistent naming

* Move new patch applied callback into accept

* Pcall archivable

* Keybinds open popup diff window

* Theme rows in diff

* Remove relic of prototype

* Color value visuals and better component name

* changeBatcher is not needed when no sync is active

* Simplify popup roact entrypoint

* Alphabetical prop lists and refactor

* Add a stroke to color blot for contrast

* Make color blots animate transparency with the rest of the page

* StyLua formatting on newly added files

* Remove wasteful table

* Fix diffing custom properties

* Display tables more meaningfully

* Allow children in the button components

* Create a rough tooltip component

* Add tooltips to buttons

* Use provider+trigger schema to avoid tooltip ZIndex issues

* Add triangle point to tooltip

* Tooltip underneath instead of covering

* Cancel hovers when unmounting

* Allow multiple canvases from one provider

* Display above or below depending on available space

* Move patch equality to PatchSet.isEqual

* Use Container

* Remove old submodules

* Reduce false positives in diff

* Add debug log

* Fuzzy equals CFrame in diffs to avoid floating point in

* Fix decodeValue usage

* Support the .changedName patches

* Fix content overlapping border

* Fix tooltip tail alignment

* Fix tooltip text fit

* Whoops, fix it properly

* Move PatchVisualizer to Components

* Provide Connected info with full patch data

* Avoid implicit nil return

* Add patch visualizer to connected page

* Make Current column invisible when visualizing applied patches

* Avoid floating point diffs in a numbers and vectors
2023-04-01 23:17:23 -04:00
ok-nick
7994bc4909 Update setup-aftman (#648) 2022-11-18 03:32:13 -05:00
boatbomber
b88d34c639 Add tooltips to buttons (#637)
* Add tooltips

* Fix whitespace

* Avoid overloaded word canvas

* Clean render function

* Switch folder to fragment
2022-10-07 19:31:14 -04:00
fox
96cb1ee3fd Support explicitly specifying http or https protocol in plugin (#642)
* Support explicitly specifying http or https protocol in plugin

* Fix incorrect format string

Port is not a number
2022-09-30 17:59:09 -04:00
boatbomber
003abe86bb Save host and port by placeId (#613)
* Save host and port by placeId

* Bump to 5 months before clearing

* Fix indentation
2022-09-22 23:03:09 -04:00
Lucien Greathouse
6ec411a618 Add Patreon badge to README 2022-08-20 23:44:10 -04:00
Qualadore
c7c0903804 Reduce minimum plugin size (#606)
* Reduce minimum plugin size

* Resize to 300x120

Co-authored-by: Qualadore <me@qualadore.com>
2022-08-20 22:40:52 -04:00
boatbomber
cdc972a5ce Migrate DevSettings to PluginSettings for much better config flow (#572)
* Add the devsetting config options into settings

* Create dropdown component and add setting controls

* Static dropdwon width and spin arrow

* Improve dropdown option contrast and border

* Forgot to make the settings page respect the static spacing, oops

* Smaller arrow

* Vert padding

* Reset option for settings

* Hide reset button when on default

* Respect the logLevel setting

* Portal settings out to external typechecking module

* Implement new configs using the new singleton Settings

* Remove DevSettings

* Update test runner to use new settings

* More helpful test failure output

* Support non-plugin environment

* Migrate dropdown to new packages system

* Clean up components a tad
2022-08-20 22:39:34 -04:00
Boegie19
17de912608 fix_vfs_double_update (#616) 2022-08-20 22:33:19 -04:00
Boegie19
9876508887 added attributes to AdjacentMetadata (#624)
* added attributes to AdjacentMetadata

* ran fmt
2022-08-20 22:32:58 -04:00
Lucien Greathouse
72d62220e8 Fix referring to open source maintainer as a chicken 2022-08-20 22:22:11 -04:00
Lucien Greathouse
46ad337fa5 Switch all workflows to Aftman 2022-08-20 22:20:51 -04:00
Boegie19
7a3ba7721f fix release action with aftman (#627)
* fix release action with aftman

* Fixes using bash not powershell

* removed comment
2022-08-20 22:15:01 -04:00
Lucien Greathouse
e0198e626b Build Linux release on Ubuntu 20.04, use fixed artifact names 2022-08-20 21:34:41 -04:00
boatbomber
142705f386 Fix security permission error (#619) 2022-08-10 15:57:24 -04:00
boatbomber
4cb49c7825 Add sync locking for Team Create (#590)
* Add sync locking

* Steal lock from users who left without releasing

* Do not remove lock as unknown instance

* Don't delete non Archivable instance
2022-08-08 04:08:55 -04:00
Barocena
05adb82dda Renamed Common to Shared (#611) 2022-08-08 03:59:52 -04:00
boatbomber
faf7671799 Make error messages copyable (#614)
* Make error copyable

* Allow partial copying or double click full copy
2022-08-08 03:58:32 -04:00
Lucien Greathouse
d64db329dd Fix release workflow to use Wally 2022-08-08 00:28:52 -04:00
Lucien Greathouse
e34d2339ad Vendor OpenSSL via native-tls-vendored reqwest feature 2022-08-08 00:15:05 -04:00
Max
d196c5091c Simplify usage of attributes. (#574)
* Support implicit values for primitive attributes

This commit adds support for strings, numbers, and booleans to be implicitly typed in attribute maps, reducing the redundancy of needing to specify their types.

I also quietly adjusted one of the tests to use a more stable class/property pair. Since SourceAssetId is locked to Roblox, it could potentially disappear at any time.

* Apply formatting.

* Address feedback

* Backwards compatible format usage.

* Axe UnresolvedValueMap in favor of $attributes

Attributes can be defined directly on instances, with support for unambiguous types.

* Adjust test.

* to_string() -> into()

* Made attribute test more concise.

* small cleanup

* Update src/resolution.rs

* Update src/resolution.rs

* Update src/resolution.rs

* Update src/resolution.rs

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-08-03 20:07:06 -04:00
Lucien Greathouse
3e83f92532 Update MSRV to 1.58.1 for format string capturing 2022-08-03 19:39:36 -04:00
James Onnen
41d7aaf323 Add uipadding to notifications (#589) 2022-08-03 19:01:07 -04:00
boatbomber
e110f3726a Real-time status about sync details (#569)
* Rough prototype of patch info display

* Remove extra newline

* Switch to binding

* Update slower for older timestamps

* Batch patches within a second of each other

* Fix indentation

* Less wasteful refresh hz

* More apt variable name

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-08-03 18:58:28 -04:00
Boegie19
eb5c897ac0 fix relevant_paths not being set for init.csv (#599)
* fix relevant_paths not being set for init.csv

* fix failing tests

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-08-03 18:38:08 -04:00
boatbomber
e864cf0c7d Switch git submodules to Wally packages (#584)
* Switch git submodules to Wally packages

* Update build snapshot

* Add wally to foreman and use latest versions

* Install packages in CI runners

* Fix indents

* Install packages in the correct directory

* Install packages in correct dir of release action too

* Remove submodules from ci checkout

* Remove submodules from release checkout

* Update selene with latest fix

* Fix whitespace

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-08-03 18:36:58 -04:00
Lucien Greathouse
565c12405e Skip empty AppliedPatchSets for sending changes. 2022-08-03 17:19:23 -04:00
JohnnyMorganz
2a6a8b42a6 Add --watch to sourcemap generation (#602)
* Implement watch argument

* Add forget call

* Clippy fixes

* Update changelog
2022-08-01 04:07:07 -04:00
Boegie19
5cb4cc0d1d feature init csv (#594)
* init csv feature + test

* fmt fixes
2022-07-29 21:45:19 -04:00
boatbomber
62eb4f026f Fix errors after session already ended (#587) 2022-07-23 12:24:16 -04:00
wackbyte
411d1a89c1 Really default to the current directory in 'rojo fmt-project' (#581)
Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-07-18 19:47:30 -04:00
boatbomber
6ae0bf366a Use singleton settings outside the Roact tree (#576)
* Use singleton settings outside the Roact tree

* Cleanup listener on unmount

* Refactor setting page components

* Fix willUnmount being added to the wrong table

* Remove bindings in favor of state
2022-07-18 19:36:38 -04:00
wackbyte
178cdc9dfa Update benches so they compile (#582) 2022-07-17 18:50:12 -04:00
wackbyte
5bf1f86886 Fix link to v7.2.1 in the changelog (#578) 2022-07-11 00:44:47 -04:00
Lucien Greathouse
e482aba030 Release v7.2.1 2022-07-08 20:22:16 -04:00
Samuel P
535e4d42bb Change Notification sound to generic sound (#566)
* Change Notification sound to generic sound

The notification sound causes the game to summon an error due to no experience permissions with no way to grant permission. This is due to the new audio policy update.

* Update Notification sound
2022-07-02 19:33:24 -04:00
boatbomber
54398d4c4b Add setting to toggle sound effects (#568)
* Use soundPlayer object with setting

* Style changes
2022-07-02 05:12:58 -04:00
601 changed files with 72675 additions and 7977 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

@@ -23,4 +23,7 @@ insert_final_newline = true
insert_final_newline = true insert_final_newline = true
[*.lua] [*.lua]
indent_style = tab
[*.luau]
indent_style = tab indent_style = tab

2
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,2 @@
# stylua formatting
0f8e1625d572a5fe0f7b5c08653ff92cc837d346

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.lua linguist-language=Luau

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
patreon: lpghatguy

23
.github/workflows/changelog.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Changelog Check
on:
pull_request:
types: [assigned, opened, synchronize, reopened, labeled, unlabeled]
branches:
- master
jobs:
build:
name: Check Actions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Changelog check
uses: Zomzog/changelog-checker@v1.3.0
with:
fileName: CHANGELOG.md
noChangelogLabel: skip changelog
checkNotification: Simple
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -12,23 +12,28 @@ on:
jobs: jobs:
build: build:
name: Build and Test name: Build and Test
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
rust_version: [stable, 1.57.0] os: [ubuntu-22.04, windows-latest, macos-latest, windows-11-arm, ubuntu-22.04-arm]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
submodules: true submodules: true
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@stable
- name: Restore Rust Cache
uses: actions/cache/restore@v4
with: with:
toolchain: ${{ matrix.rust_version }} path: |
override: true ~/.cargo/registry
profile: minimal ~/.cargo/git
target
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Build - name: Build
run: cargo build --locked --verbose run: cargo build --locked --verbose
@@ -36,24 +41,93 @@ jobs:
- name: Test - name: Test
run: cargo test --locked --verbose run: cargo test --locked --verbose
lint: - name: Save Rust Cache
name: Rustfmt and Clippy uses: actions/cache/save@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
msrv:
name: Check MSRV
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
submodules: true submodules: true
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@1.88.0
- name: Restore Rust Cache
uses: actions/cache/restore@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Build
run: cargo build --locked --verbose
- name: Save Rust Cache
uses: actions/cache/save@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
lint:
name: Rustfmt, Clippy, Stylua, & Selene
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: stable
override: true
components: rustfmt, clippy components: rustfmt, clippy
- name: Restore Rust Cache
uses: actions/cache/restore@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Setup Rokit
uses: CompeyDev/setup-rokit@v0.1.2
with:
version: 'v1.1.0'
- name: Stylua
run: stylua --check plugin/src
- name: Selene
run: selene plugin/src
- name: Rustfmt - name: Rustfmt
run: cargo fmt -- --check run: cargo fmt -- --check
- 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

@@ -8,49 +8,39 @@ jobs:
create-release: create-release:
name: Create Release name: Create Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps: steps:
- uses: actions/checkout@v4
- name: Create Release - name: Create Release
id: create_release
uses: actions/create-release@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: run: |
tag_name: ${{ github.ref }} gh release create ${{ github.ref_name }} --draft --verify-tag --title ${{ github.ref_name }}
release_name: ${{ github.ref }}
draft: true
prerelease: false
build-plugin: build-plugin:
needs: ["create-release"] needs: ["create-release"]
name: Build Roblox Studio Plugin name: Build Roblox Studio Plugin
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
submodules: true submodules: true
- name: Setup Foreman - name: Setup Rokit
uses: Roblox/setup-foreman@v1 uses: CompeyDev/setup-rokit@v0.1.2
with: with:
token: ${{ secrets.GITHUB_TOKEN }} version: 'v1.1.0'
- 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
uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: run: |
upload_url: ${{ needs.create-release.outputs.upload_url }} gh release upload ${{ github.ref_name }} Rojo.rbxm
asset_path: Rojo.rbxm
asset_name: Rojo.rbxm
asset_content_type: application/octet-stream
- name: Upload Plugin to Artifacts - name: Upload Plugin to Artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: Rojo.rbxm name: Rojo.rbxm
path: Rojo.rbxm path: Rojo.rbxm
@@ -61,25 +51,31 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
# https://doc.rust-lang.org/rustc/platform-support.html # https://doc.rust-lang.org/rustc/platform-support.html
#
# FIXME: After the Rojo VS Code extension updates, add architecture
# names to each of these releases. We'll rename win64 to windows and add
# -x86_64 to each release.
include: include:
- host: linux - host: linux
os: ubuntu-18.04 os: ubuntu-22.04
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
label: linux label: linux-x86_64
- host: linux
os: ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu
label: linux-aarch64
- host: windows - host: windows
os: windows-latest os: windows-latest
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
label: win64 label: windows-x86_64
- host: windows
os: windows-11-arm
target: aarch64-pc-windows-msvc
label: windows-aarch64
- host: macos - host: macos
os: macos-latest os: macos-latest
target: x86_64-apple-darwin target: x86_64-apple-darwin
label: macos label: macos-x86_64
- host: macos - host: macos
os: macos-latest os: macos-latest
@@ -91,63 +87,64 @@ jobs:
env: env:
BIN: rojo BIN: rojo
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
submodules: true submodules: true
- name: Get Version from Tag
shell: bash
# https://github.community/t/how-to-get-just-the-tag-name/16241/7#M1027
run: |
echo "PROJECT_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
echo "Version is: ${{ env.PROJECT_VERSION }}"
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: stable targets: ${{ matrix.target }}
target: ${{ matrix.target }}
override: true - name: Restore Rust Cache
profile: minimal uses: actions/cache/restore@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Build Release - name: Build Release
run: cargo build --release --locked --verbose run: cargo build --release --locked --verbose --target ${{ matrix.target }}
env:
# Build into a known directory so we can find our build artifact more
# easily.
CARGO_TARGET_DIR: output
# On platforms that use OpenSSL, ensure it is statically linked to - name: Save Rust Cache
# make binaries more portable. uses: actions/cache/save@v4
OPENSSL_STATIC: 1 with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Create Release Archive - name: Generate Artifact Name
shell: bash shell: bash
env:
TAG_NAME: ${{ github.ref_name }}
run: |
echo "ARTIFACT_NAME=$BIN-${TAG_NAME#v}-${{ matrix.label }}.zip" >> "$GITHUB_ENV"
- name: Create Archive and Upload to Release
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
mkdir staging mkdir staging
if [ "${{ matrix.host }}" = "windows" ]; then if [ "${{ matrix.host }}" = "windows" ]; then
cp "output/release/$BIN.exe" staging/ cp "target/${{ matrix.target }}/release/$BIN.exe" staging/
cd staging cd staging
7z a ../release.zip * 7z a ../$ARTIFACT_NAME *
else else
cp "output/release/$BIN" staging/ cp "target/${{ matrix.target }}/release/$BIN" staging/
cd staging cd staging
zip ../release.zip * zip ../$ARTIFACT_NAME *
fi fi
- name: Upload Archive to Release gh release upload ${{ github.ref_name }} ../$ARTIFACT_NAME
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: release.zip
asset_name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
asset_content_type: application/octet-stream
- name: Upload Archive to Artifacts - name: Upload Archive to Artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip path: ${{ env.ARTIFACT_NAME }}
path: release.zip name: ${{ env.ARTIFACT_NAME }}

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

31
.gitmodules vendored
View File

@@ -1,15 +1,18 @@
[submodule "plugin/modules/roact"] [submodule "plugin/Packages/Roact"]
path = plugin/modules/roact path = plugin/Packages/Roact
url = https://github.com/Roblox/roact.git url = https://github.com/roblox/roact.git
[submodule "plugin/modules/testez"] [submodule "plugin/Packages/Flipper"]
path = plugin/modules/testez path = plugin/Packages/Flipper
url = https://github.com/Roblox/testez.git url = https://github.com/reselim/flipper.git
[submodule "plugin/modules/promise"] [submodule "plugin/Packages/Promise"]
path = plugin/modules/promise path = plugin/Packages/Promise
url = https://github.com/LPGhatguy/roblox-lua-promise.git url = https://github.com/evaera/roblox-lua-promise.git
[submodule "plugin/modules/t"] [submodule "plugin/Packages/t"]
path = plugin/modules/t path = plugin/Packages/t
url = https://github.com/osyrisrblx/t.git url = https://github.com/osyrisrblx/t.git
[submodule "plugin/modules/flipper"] [submodule "plugin/Packages/TestEZ"]
path = plugin/modules/flipper path = plugin/Packages/TestEZ
url = https://github.com/Reselim/Flipper url = https://github.com/roblox/testez.git
[submodule "plugin/Packages/Highlighter"]
path = plugin/Packages/Highlighter
url = https://github.com/boatbomber/highlighter.git

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
}

File diff suppressed because it is too large Load Diff

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.

2585
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,12 @@
[package] [package]
name = "rojo" name = "rojo"
version = "7.2.0" version = "7.7.0-rc.1"
rust-version = "1.57.0" rust-version = "1.88"
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"
@@ -12,9 +16,7 @@ readme = "README.md"
edition = "2021" edition = "2021"
build = "build.rs" build = "build.rs"
exclude = [ exclude = ["/test-projects/**"]
"/test-projects/**",
]
[profile.dev] [profile.dev]
panic = "abort" panic = "abort"
@@ -28,7 +30,9 @@ default = []
# Enable this feature to live-reload assets from the web UI. # Enable this feature to live-reload assets from the web UI.
dev_live_assets = [] dev_live_assets = []
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client"] # Run Rojo with this feature to open a Tracy session.
# Currently uses protocol v63, last supported in Tracy 0.9.1.
profile-with-tracy = ["profiling/profile-with-tracy"]
[workspace] [workspace]
members = ["crates/*"] members = ["crates/*"]
@@ -42,69 +46,85 @@ name = "build"
harness = false harness = false
[dependencies] [dependencies]
memofs = { version = "0.2.0", path = "crates/memofs" } memofs = { version = "0.3.1", path = "crates/memofs" }
# These dependencies can be uncommented when working on rbx-dom simultaneously # These dependencies can be uncommented when working on rbx-dom simultaneously
# rbx_binary = { path = "../rbx-dom/rbx_binary" } # rbx_binary = { path = "../rbx-dom/rbx_binary", features = [
# "unstable_text_format",
# ] }
# rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" } # rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" }
# rbx_reflection = { path = "../rbx-dom/rbx_reflection" } # rbx_reflection = { path = "../rbx-dom/rbx_reflection" }
# 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.6.5" rbx_binary = { version = "2.0.1", features = ["unstable_text_format"] }
rbx_dom_weak = "2.4.0" rbx_dom_weak = "4.1.0"
rbx_reflection = "4.2.0" rbx_reflection = "6.1.0"
rbx_reflection_database = "0.2.2" rbx_reflection_database = "2.0.2"
rbx_xml = "0.12.3" rbx_xml = "2.0.1"
anyhow = "1.0.44" anyhow = "1.0.80"
backtrace = "0.3.61" backtrace = "0.3.69"
bincode = "1.3.3" bincode = "1.3.3"
crossbeam-channel = "0.5.1" crossbeam-channel = "0.5.12"
csv = "1.1.6" csv = "1.3.0"
env_logger = "0.9.0" env_logger = "0.9.3"
fs-err = "2.6.0" fs-err = "2.11.0"
futures = "0.3.17" futures = "0.3.30"
globset = "0.4.8" globset = "0.4.14"
humantime = "2.1.0" humantime = "2.1.0"
hyper = { version = "0.14.13", features = ["server", "tcp", "http1"] } hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
hyper-tungstenite = "0.11.0"
jod-thread = "0.1.2" jod-thread = "0.1.2"
log = "0.4.14" log = "0.4.21"
maplit = "1.0.2" num_cpus = "1.16.0"
notify = "4.0.17" opener = "0.5.2"
opener = "0.5.0" rayon = "1.9.0"
reqwest = { version = "0.11.10", features = ["blocking", "json"] } reqwest = { version = "0.11.24", default-features = false, features = [
"blocking",
"json",
"rustls-tls",
] }
ritz = "0.1.0" ritz = "0.1.0"
roblox_install = "1.0.0" roblox_install = "1.0.0"
serde = { version = "1.0.130", features = ["derive", "rc"] } serde = { version = "1.0.197", features = ["derive", "rc"] }
serde_json = "1.0.68" serde_json = "1.0.145"
termcolor = "1.1.2" jsonc-parser = { version = "0.27.0", features = ["serde"] }
thiserror = "1.0.30" strum = { version = "0.27", features = ["derive"] }
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] } toml = "0.5.11"
uuid = { version = "1.0.0", features = ["v4", "serde"] } termcolor = "1.4.1"
clap = { version = "3.1.18", features = ["derive"] } thiserror = "1.0.57"
profiling = "1.0.6" tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "macros"] }
tracy-client = { version = "0.13.2", optional = true } uuid = { version = "1.7.0", features = ["v4", "serde"] }
clap = { version = "3.2.25", features = ["derive"] }
profiling = "1.0.15"
yaml-rust2 = "0.10.3"
data-encoding = "2.8.0"
blake3 = "1.5.0"
float-cmp = "0.9.0"
indexmap = { version = "2.10.0", features = ["serde"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winreg = "0.10.1" winreg = "0.10.1"
[build-dependencies] [build-dependencies]
memofs = { version = "0.2.0", path = "crates/memofs" } memofs = { version = "0.3.0", path = "crates/memofs" }
embed-resource = "1.6.4" embed-resource = "1.8.0"
anyhow = "1.0.44" anyhow = "1.0.80"
bincode = "1.3.3" bincode = "1.3.3"
fs-err = "2.6.0" fs-err = "2.11.0"
maplit = "1.0.2" maplit = "1.0.2"
semver = "1.0.22"
[dev-dependencies] [dev-dependencies]
rojo-insta-ext = { path = "crates/rojo-insta-ext" } rojo-insta-ext = { path = "crates/rojo-insta-ext" }
criterion = "0.3.5" criterion = "0.3.6"
insta = { version = "1.8.0", features = ["redactions"] } insta = { version = "1.36.1", features = ["redactions", "yaml"] }
paste = "1.0.5" paste = "1.0.14"
pretty_assertions = "1.2.1" pretty_assertions = "1.4.0"
serde_yaml = "0.8.21" serde_yaml = "0.8.26"
tempfile = "3.2.0" tempfile = "3.10.1"
walkdir = "2.3.2" walkdir = "2.5.0"

View File

@@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<a href="https://rojo.space"><img src="assets/logo-512.png" alt="Rojo" height="217" /></a> <a href="https://rojo.space"><img src="assets/brand_images/logo-512.png" alt="Rojo" height="217" /></a>
</div> </div>
<div>&nbsp;</div> <div>&nbsp;</div>
@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
Pull requests are welcome! Pull requests are welcome!
Rojo supports Rust 1.57.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has. Rojo supports Rust 1.88 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
## License ## License
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details. Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.

View File

Before

Width:  |  Height:  |  Size: 975 B

After

Width:  |  Height:  |  Size: 975 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 229 B

After

Width:  |  Height:  |  Size: 229 B

View File

Before

Width:  |  Height:  |  Size: 584 B

After

Width:  |  Height:  |  Size: 584 B

View File

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 295 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

View File

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

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

@@ -2,7 +2,7 @@
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}. Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
## Getting Started ## Getting Started
To build this library or plugin, use: To build this library, use:
```bash ```bash
rojo build -o "{project_name}.rbxmx" rojo build -o "{project_name}.rbxmx"

View File

@@ -1,3 +1,5 @@
# Roblox Studio lock files # Roblox Studio lock files
/*.rbxlx.lock /*.rbxlx.lock
/*.rbxl.lock /*.rbxl.lock
sourcemap.json

View File

@@ -2,4 +2,4 @@ return {
hello = function() hello = function()
print("Hello world, from {project_name}!") print("Hello world, from {project_name}!")
end, end,
} }

View File

@@ -4,7 +4,7 @@
"$className": "DataModel", "$className": "DataModel",
"ReplicatedStorage": { "ReplicatedStorage": {
"Common": { "Shared": {
"$path": "src/shared" "$path": "src/shared"
} }
}, },

View File

@@ -3,4 +3,6 @@
# Roblox Studio lock files # Roblox Studio lock files
/*.rbxlx.lock /*.rbxlx.lock
/*.rbxl.lock /*.rbxl.lock
sourcemap.json

View File

@@ -0,0 +1 @@
print("Hello world, from client!")

View File

@@ -0,0 +1 @@
print("Hello world, from server!")

View File

@@ -0,0 +1,3 @@
return function()
print("Hello, world!")
end

View File

@@ -0,0 +1,17 @@
# {project_name}
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
## Getting Started
To build this plugin to your local plugins folder, use:
```bash
rojo build -p "{project_name}.rbxm"
```
You can include the `watch` flag to re-build it on save:
```bash
rojo build -p "{project_name}.rbxm" --watch
```
For more help, check out [the Rojo documentation](https://rojo.space/docs).

View File

@@ -0,0 +1,6 @@
{
"name": "{project_name}",
"tree": {
"$path": "src"
}
}

View File

@@ -0,0 +1,5 @@
# Plugin model files
/{project_name}.rbxmx
/{project_name}.rbxm
sourcemap.json

View File

@@ -0,0 +1 @@
print("Hello world, from plugin!")

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
use librojo::cli::{build, BuildCommand}; use librojo::cli::BuildCommand;
pub fn benchmark_small_place(c: &mut Criterion) { pub fn benchmark_small_place(c: &mut Criterion) {
bench_build_place(c, "Small Place", "test-projects/benchmark_small_place") bench_build_place(c, "Small Place", "test-projects/benchmark_small_place")
@@ -20,7 +20,7 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
group.bench_function("build", |b| { group.bench_function("build", |b| {
b.iter_batched( b.iter_batched(
|| place_setup(path), || place_setup(path),
|(_dir, options)| build(options).unwrap(), |(_dir, options)| options.run().unwrap(),
BatchSize::SmallInput, BatchSize::SmallInput,
) )
}); });
@@ -31,11 +31,12 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) { fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let input = input_path.as_ref().to_path_buf(); let input = input_path.as_ref().to_path_buf();
let output = dir.path().join("output.rbxlx"); let output = Some(dir.path().join("output.rbxlx"));
let options = BuildCommand { let options = BuildCommand {
project: input, project: input,
watch: false, watch: false,
plugin: None,
output, output,
}; };

View File

@@ -7,6 +7,7 @@ use fs_err as fs;
use fs_err::File; use fs_err::File;
use maplit::hashmap; use maplit::hashmap;
use memofs::VfsSnapshot; use memofs::VfsSnapshot;
use semver::Version;
fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> { fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
println!("cargo:rerun-if-changed={}", path.display()); println!("cargo:rerun-if-changed={}", path.display());
@@ -19,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") {
@@ -40,38 +45,39 @@ 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 templates_dir = root_dir.join("assets").join("project-templates");
let plugin_modules = plugin_root.join("modules"); let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?;
let plugin_version =
Version::parse(fs::read_to_string(plugin_dir.join("Version.txt"))?.trim())?;
let snapshot = VfsSnapshot::dir(hashmap! { assert_eq!(
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?, our_version, plugin_version,
"fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?, "plugin version does not match Cargo version"
"http" => snapshot_from_fs_path(&plugin_root.join("http"))?, );
"log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?, let template_snapshot = snapshot_from_fs_path(&templates_dir)?;
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
"modules" => VfsSnapshot::dir(hashmap! { let plugin_snapshot = VfsSnapshot::dir(hashmap! {
"roact" => VfsSnapshot::dir(hashmap! { "default.project.json" => snapshot_from_fs_path(&root_dir.join("plugin.project.json"))?,
"src" => snapshot_from_fs_path(&plugin_modules.join("roact").join("src"))? "plugin" => VfsSnapshot::dir(hashmap! {
}), "fmt" => snapshot_from_fs_path(&plugin_dir.join("fmt"))?,
"promise" => VfsSnapshot::dir(hashmap! { "http" => snapshot_from_fs_path(&plugin_dir.join("http"))?,
"lib" => snapshot_from_fs_path(&plugin_modules.join("promise").join("lib"))? "log" => snapshot_from_fs_path(&plugin_dir.join("log"))?,
}), "rbx_dom_lua" => snapshot_from_fs_path(&plugin_dir.join("rbx_dom_lua"))?,
"t" => VfsSnapshot::dir(hashmap! { "src" => snapshot_from_fs_path(&plugin_dir.join("src"))?,
"lib" => snapshot_from_fs_path(&plugin_modules.join("t").join("lib"))? "Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?,
}), "Version.txt" => snapshot_from_fs_path(&plugin_dir.join("Version.txt"))?,
"flipper" => VfsSnapshot::dir(hashmap! {
"src" => snapshot_from_fs_path(&plugin_modules.join("flipper").join("src"))?
}),
}), }),
}); });
let out_path = Path::new(&out_dir).join("plugin.bincode"); let template_file = File::create(Path::new(&out_dir).join("templates.bincode"))?;
let out_file = File::create(&out_path)?; let plugin_file = File::create(Path::new(&out_dir).join("plugin.bincode"))?;
bincode::serialize_into(out_file, &snapshot)?; bincode::serialize_into(plugin_file, &plugin_snapshot)?;
bincode::serialize_into(template_file, &template_snapshot)?;
println!("cargo:rerun-if-changed=build/windows/rojo-manifest.rc"); println!("cargo:rerun-if-changed=build/windows/rojo-manifest.rc");
println!("cargo:rerun-if-changed=build/windows/rojo.manifest"); println!("cargo:rerun-if-changed=build/windows/rojo.manifest");

View File

@@ -2,6 +2,20 @@
## Unreleased Changes ## Unreleased Changes
## 0.3.1 (2025-11-27)
* Added `Vfs::exists`. [#1169]
* Added `create_dir` and `create_dir_all` to allow creating directories. [#937]
[#1169]: https://github.com/rojo-rbx/rojo/pull/1169
[#937]: https://github.com/rojo-rbx/rojo/pull/937
## 0.3.0 (2024-03-15)
* Changed `StdBackend` file watching component to use minimal recursive watches. [#830]
* Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854]
[#830]: https://github.com/rojo-rbx/rojo/pull/830
[#854]: https://github.com/rojo-rbx/rojo/pull/854
## 0.2.0 (2021-08-23) ## 0.2.0 (2021-08-23)
* Updated to `crossbeam-channel` 0.5.1. * Updated to `crossbeam-channel` 0.5.1.
@@ -15,4 +29,4 @@
* Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate. * Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate.
## 0.1.0 (2020-03-10) ## 0.1.0 (2020-03-10)
* Initial release * Initial release

View File

@@ -1,8 +1,12 @@
[package] [package]
name = "memofs" name = "memofs"
description = "Virtual filesystem with configurable backends." description = "Virtual filesystem with configurable backends."
version = "0.2.0" version = "0.3.1"
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"
@@ -11,7 +15,7 @@ homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
crossbeam-channel = "0.5.1" crossbeam-channel = "0.5.12"
fs-err = "2.3.0" fs-err = "2.11.0"
notify = "4.0.15" notify = "4.0.17"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }

View File

@@ -50,6 +50,12 @@ impl InMemoryFs {
} }
} }
impl Default for InMemoryFs {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)] #[derive(Debug)]
struct InMemoryFsInner { struct InMemoryFsInner {
entries: HashMap<PathBuf, Entry>, entries: HashMap<PathBuf, Entry>,
@@ -151,6 +157,11 @@ impl VfsBackend for InMemoryFs {
) )
} }
fn exists(&mut self, path: &Path) -> io::Result<bool> {
let inner = self.inner.lock().unwrap();
Ok(inner.entries.contains_key(path))
}
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> { fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
let inner = self.inner.lock().unwrap(); let inner = self.inner.lock().unwrap();
@@ -170,6 +181,21 @@ impl VfsBackend for InMemoryFs {
} }
} }
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
let mut inner = self.inner.lock().unwrap();
inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir())
}
fn create_dir_all(&mut self, path: &Path) -> io::Result<()> {
let mut inner = self.inner.lock().unwrap();
let mut path_buf = path.to_path_buf();
while let Some(parent) = path_buf.parent() {
inner.load_snapshot(parent.to_path_buf(), VfsSnapshot::empty_dir())?;
path_buf.pop();
}
inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir())
}
fn remove_file(&mut self, path: &Path) -> io::Result<()> { fn remove_file(&mut self, path: &Path) -> io::Result<()> {
let mut inner = self.inner.lock().unwrap(); let mut inner = self.inner.lock().unwrap();
@@ -222,23 +248,17 @@ impl VfsBackend for InMemoryFs {
} }
fn must_be_file<T>(path: &Path) -> io::Result<T> { fn must_be_file<T>(path: &Path) -> io::Result<T> {
Err(io::Error::new( Err(io::Error::other(format!(
io::ErrorKind::Other, "path {} was a directory, but must be a file",
format!( path.display()
"path {} was a directory, but must be a file", )))
path.display()
),
))
} }
fn must_be_dir<T>(path: &Path) -> io::Result<T> { fn must_be_dir<T>(path: &Path) -> io::Result<T> {
Err(io::Error::new( Err(io::Error::other(format!(
io::ErrorKind::Other, "path {} was a file, but must be a directory",
format!( path.display()
"path {} was a file, but must be a directory", )))
path.display()
),
))
} }
fn not_found<T>(path: &Path) -> io::Result<T> { fn not_found<T>(path: &Path) -> io::Result<T> {

View File

@@ -22,9 +22,9 @@ mod noop_backend;
mod snapshot; mod snapshot;
mod std_backend; mod std_backend;
use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::{Arc, Mutex, MutexGuard};
use std::{io, str};
pub use in_memory_fs::InMemoryFs; pub use in_memory_fs::InMemoryFs;
pub use noop_backend::NoopBackend; pub use noop_backend::NoopBackend;
@@ -70,7 +70,10 @@ impl<T> IoResultExt<T> for io::Result<T> {
pub trait VfsBackend: sealed::Sealed + Send + 'static { pub trait VfsBackend: sealed::Sealed + Send + 'static {
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>>; fn read(&mut self, path: &Path) -> io::Result<Vec<u8>>;
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>; fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>;
fn exists(&mut self, path: &Path) -> io::Result<bool>;
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir>; fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir>;
fn create_dir(&mut self, path: &Path) -> io::Result<()>;
fn create_dir_all(&mut self, path: &Path) -> io::Result<()>;
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>; fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
fn remove_file(&mut self, path: &Path) -> io::Result<()>; fn remove_file(&mut self, path: &Path) -> io::Result<()>;
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>; fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
@@ -155,6 +158,29 @@ impl VfsInner {
Ok(Arc::new(contents)) Ok(Arc::new(contents))
} }
fn read_to_string<P: AsRef<Path>>(&mut self, path: P) -> io::Result<Arc<String>> {
let path = path.as_ref();
let contents = self.backend.read(path)?;
if self.watch_enabled {
self.backend.watch(path)?;
}
let contents_str = str::from_utf8(&contents).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("File was not valid UTF-8: {}", path.display()),
)
})?;
Ok(Arc::new(contents_str.into()))
}
fn exists<P: AsRef<Path>>(&mut self, path: P) -> io::Result<bool> {
let path = path.as_ref();
self.backend.exists(path)
}
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> { fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> {
let path = path.as_ref(); let path = path.as_ref();
let contents = contents.as_ref(); let contents = contents.as_ref();
@@ -172,6 +198,16 @@ impl VfsInner {
Ok(dir) Ok(dir)
} }
fn create_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.backend.create_dir(path)
}
fn create_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.backend.create_dir_all(path)
}
fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> { fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref(); let path = path.as_ref();
let _ = self.backend.unwatch(path); let _ = self.backend.unwatch(path);
@@ -194,11 +230,8 @@ impl VfsInner {
} }
fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> { fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
match event { if let VfsEvent::Remove(path) = event {
VfsEvent::Remove(path) => { let _ = self.backend.unwatch(path);
let _ = self.backend.unwatch(&path);
}
_ => {}
} }
Ok(()) Ok(())
@@ -261,6 +294,33 @@ impl Vfs {
self.inner.lock().unwrap().read(path) self.inner.lock().unwrap().read(path)
} }
/// Read a file from the VFS (or from the underlying backend if it isn't
/// resident) into a string.
///
/// Roughly equivalent to [`std::fs::read_to_string`][std::fs::read_to_string].
///
/// [std::fs::read_to_string]: https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html
#[inline]
pub fn read_to_string<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<String>> {
let path = path.as_ref();
self.inner.lock().unwrap().read_to_string(path)
}
/// Read a file from the VFS (or the underlying backend if it isn't
/// resident) into a string, and normalize its line endings to LF.
///
/// Roughly equivalent to [`std::fs::read_to_string`][std::fs::read_to_string], but also performs
/// line ending normalization.
///
/// [std::fs::read_to_string]: https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html
#[inline]
pub fn read_to_string_lf_normalized<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<String>> {
let path = path.as_ref();
let contents = self.inner.lock().unwrap().read_to_string(path)?;
Ok(contents.replace("\r\n", "\n").into())
}
/// Write a file to the VFS and the underlying backend. /// Write a file to the VFS and the underlying backend.
/// ///
/// Roughly equivalent to [`std::fs::write`][std::fs::write]. /// Roughly equivalent to [`std::fs::write`][std::fs::write].
@@ -284,6 +344,42 @@ impl Vfs {
self.inner.lock().unwrap().read_dir(path) self.inner.lock().unwrap().read_dir(path)
} }
/// Return whether the given path exists.
///
/// Roughly equivalent to [`std::fs::exists`][std::fs::exists].
///
/// [std::fs::exists]: https://doc.rust-lang.org/stable/std/fs/fn.exists.html
#[inline]
pub fn exists<P: AsRef<Path>>(&self, path: P) -> io::Result<bool> {
let path = path.as_ref();
self.inner.lock().unwrap().exists(path)
}
/// Creates a directory at the provided location.
///
/// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir].
/// Similiar to that function, this function will fail if the parent of the
/// path does not exist.
///
/// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html
#[inline]
pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.inner.lock().unwrap().create_dir(path)
}
/// Creates a directory at the provided location, recursively creating
/// all parent components if they are missing.
///
/// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all].
///
/// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html
#[inline]
pub fn create_dir_all<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.inner.lock().unwrap().create_dir_all(path)
}
/// Remove a file. /// Remove a file.
/// ///
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file]. /// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
@@ -386,6 +482,31 @@ impl VfsLock<'_> {
self.inner.read_dir(path) self.inner.read_dir(path)
} }
/// Creates a directory at the provided location.
///
/// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir].
/// Similiar to that function, this function will fail if the parent of the
/// path does not exist.
///
/// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html
#[inline]
pub fn create_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.inner.create_dir(path)
}
/// Creates a directory at the provided location, recursively creating
/// all parent components if they are missing.
///
/// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all].
///
/// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html
#[inline]
pub fn create_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.inner.create_dir_all(path)
}
/// Remove a file. /// Remove a file.
/// ///
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file]. /// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
@@ -431,3 +552,23 @@ impl VfsLock<'_> {
self.inner.commit_event(event) self.inner.commit_event(event)
} }
} }
#[cfg(test)]
mod test {
use crate::{InMemoryFs, Vfs, VfsSnapshot};
/// https://github.com/rojo-rbx/rojo/issues/899
#[test]
fn read_to_string_lf_normalized_keeps_trailing_newline() {
let mut imfs = InMemoryFs::new();
imfs.load_snapshot("test", VfsSnapshot::file("bar\r\nfoo\r\n\r\n"))
.unwrap();
let vfs = Vfs::new(imfs);
assert_eq!(
vfs.read_to_string_lf_normalized("test").unwrap().as_str(),
"bar\nfoo\n\n"
);
}
}

View File

@@ -15,45 +15,39 @@ impl NoopBackend {
impl VfsBackend for NoopBackend { impl VfsBackend for NoopBackend {
fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> { fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> {
Err(io::Error::new( Err(io::Error::other("NoopBackend doesn't do anything"))
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
} }
fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> { fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> {
Err(io::Error::new( Err(io::Error::other("NoopBackend doesn't do anything"))
io::ErrorKind::Other, }
"NoopBackend doesn't do anything",
)) fn exists(&mut self, _path: &Path) -> io::Result<bool> {
Err(io::Error::other("NoopBackend doesn't do anything"))
} }
fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> { fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> {
Err(io::Error::new( Err(io::Error::other("NoopBackend doesn't do anything"))
io::ErrorKind::Other, }
"NoopBackend doesn't do anything",
)) fn create_dir(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
}
fn create_dir_all(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
} }
fn remove_file(&mut self, _path: &Path) -> io::Result<()> { fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::new( Err(io::Error::other("NoopBackend doesn't do anything"))
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
} }
fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> { fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::new( Err(io::Error::other("NoopBackend doesn't do anything"))
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
} }
fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> { fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> {
Err(io::Error::new( Err(io::Error::other("NoopBackend doesn't do anything"))
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
} }
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
@@ -61,16 +55,16 @@ impl VfsBackend for NoopBackend {
} }
fn watch(&mut self, _path: &Path) -> io::Result<()> { fn watch(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::new( Err(io::Error::other("NoopBackend doesn't do anything"))
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
} }
fn unwatch(&mut self, _path: &Path) -> io::Result<()> { fn unwatch(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::new( Err(io::Error::other("NoopBackend doesn't do anything"))
io::ErrorKind::Other, }
"NoopBackend doesn't do anything", }
))
impl Default for NoopBackend {
fn default() -> Self {
Self::new()
} }
} }

View File

@@ -1,8 +1,8 @@
use std::io; use std::path::{Path, PathBuf};
use std::path::Path;
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use std::{collections::HashSet, io};
use crossbeam_channel::Receiver; use crossbeam_channel::Receiver;
use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
@@ -13,6 +13,7 @@ use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent};
pub struct StdBackend { pub struct StdBackend {
watcher: RecommendedWatcher, watcher: RecommendedWatcher,
watcher_receiver: Receiver<VfsEvent>, watcher_receiver: Receiver<VfsEvent>,
watches: HashSet<PathBuf>,
} }
impl StdBackend { impl StdBackend {
@@ -48,6 +49,7 @@ impl StdBackend {
Self { Self {
watcher, watcher,
watcher_receiver: rx, watcher_receiver: rx,
watches: HashSet::new(),
} }
} }
} }
@@ -61,6 +63,10 @@ impl VfsBackend for StdBackend {
fs_err::write(path, data) fs_err::write(path, data)
} }
fn exists(&mut self, path: &Path) -> io::Result<bool> {
std::fs::exists(path)
}
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> { fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect(); let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect();
let mut entries = entries?; let mut entries = entries?;
@@ -76,6 +82,14 @@ impl VfsBackend for StdBackend {
}) })
} }
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
fs_err::create_dir(path)
}
fn create_dir_all(&mut self, path: &Path) -> io::Result<()> {
fs_err::create_dir_all(path)
}
fn remove_file(&mut self, path: &Path) -> io::Result<()> { fn remove_file(&mut self, path: &Path) -> io::Result<()> {
fs_err::remove_file(path) fs_err::remove_file(path)
} }
@@ -97,14 +111,28 @@ impl VfsBackend for StdBackend {
} }
fn watch(&mut self, path: &Path) -> io::Result<()> { fn watch(&mut self, path: &Path) -> io::Result<()> {
self.watcher if self.watches.contains(path)
.watch(path, RecursiveMode::NonRecursive) || path
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner)) .ancestors()
.any(|ancestor| self.watches.contains(ancestor))
{
Ok(())
} else {
self.watches.insert(path.to_path_buf());
self.watcher
.watch(path, RecursiveMode::Recursive)
.map_err(io::Error::other)
}
} }
fn unwatch(&mut self, path: &Path) -> io::Result<()> { fn unwatch(&mut self, path: &Path) -> io::Result<()> {
self.watcher self.watches.remove(path);
.unwatch(path) self.watcher.unwatch(path).map_err(io::Error::other)
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner)) }
}
impl Default for StdBackend {
fn default() -> Self {
Self::new()
} }
} }

View File

@@ -6,5 +6,5 @@ edition = "2018"
publish = false publish = false
[dependencies] [dependencies]
serde = "1.0.99" serde = "1.0.197"
serde_yaml = "0.8.9" serde_yaml = "0.8.26"

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

@@ -1,4 +0,0 @@
[tools]
rojo = { source = "rojo-rbx/rojo", version = "7.1.1" }
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }
selene = { source = "Kampfkarren/selene", version = "0.18.2" }

27
plugin.project.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "Rojo",
"tree": {
"$className": "Folder",
"Plugin": {
"$path": "plugin/src"
},
"Packages": {
"$path": "plugin/Packages",
"Log": {
"$path": "plugin/log"
},
"Http": {
"$path": "plugin/http"
},
"Fmt": {
"$path": "plugin/fmt"
},
"RbxDom": {
"$path": "plugin/rbx_dom_lua"
}
},
"Version": {
"$path": "plugin/Version.txt"
}
}
}

1
plugin/Packages/Roact Submodule

Submodule plugin/Packages/Roact added at 956891b70f

1
plugin/Packages/t Submodule

Submodule plugin/Packages/t added at 1dbfccc182

1
plugin/Version.txt Normal file
View File

@@ -0,0 +1 @@
7.7.0-rc.1

View File

@@ -1,33 +0,0 @@
{
"name": "Rojo",
"tree": {
"$className": "Folder",
"Plugin": {
"$path": "src"
},
"Log": {
"$path": "log"
},
"Http": {
"$path": "http"
},
"Fmt": {
"$path": "fmt"
},
"RbxDom": {
"$path": "rbx_dom_lua"
},
"Roact": {
"$path": "modules/roact/src"
},
"Promise": {
"$path": "modules/promise/lib"
},
"t": {
"$path": "modules/t/lib"
},
"Flipper": {
"$path": "modules/flipper/src"
}
}
}

View File

@@ -25,7 +25,7 @@
local function defaultTableDebug(buffer, input) local function defaultTableDebug(buffer, input)
buffer:writeRaw("{") buffer:writeRaw("{")
for key, value in pairs(input) do for key, value in input do
buffer:write("[{:?}] = {:?}", key, value) buffer:write("[{:?}] = {:?}", key, value)
if next(input, key) ~= nil then if next(input, key) ~= nil then
@@ -50,7 +50,7 @@ local function defaultTableDebugExtended(buffer, input)
buffer:writeLineRaw("{") buffer:writeLineRaw("{")
buffer:indent() buffer:indent()
for key, value in pairs(input) do for key, value in input do
buffer:writeLine("[{:?}] = {:#?},", key, value) buffer:writeLine("[{:?}] = {:#?},", key, value)
end end
@@ -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

@@ -3,12 +3,12 @@ Error.__index = Error
Error.Kind = { Error.Kind = {
HttpNotEnabled = { HttpNotEnabled = {
message = "Rojo requires HTTP access, which is not enabled.\n" .. message = "Rojo requires HTTP access, which is not enabled.\n"
"Check your game settings, located in the 'Home' tab of Studio.", .. "Check your game settings, located in the 'Home' tab of Studio.",
}, },
ConnectFailed = { ConnectFailed = {
message = "Couldn't connect to the Rojo server.\n" .. message = "Couldn't connect to the Rojo server.\n"
"Make sure the server is running — use 'rojo serve' to run it!", .. "Make sure the server is running — use 'rojo serve' to run it!",
}, },
Timeout = { Timeout = {
message = "HTTP request timed out.", message = "HTTP request timed out.",
@@ -63,4 +63,13 @@ function Error.fromRobloxErrorString(message)
return Error.new(Error.Kind.Unknown, message) return Error.new(Error.Kind.Unknown, message)
end end
function Error.fromResponse(response)
local lower = (response.body or ""):lower()
if response.code == 408 or response.code == 504 or lower:find("timed? ?out") then
return Error.new(Error.Kind.Timeout)
end
return Error.new(Error.Kind.Unknown, string.format("%s: %s", tostring(response.code), tostring(response.body)))
end
return Error return Error

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

@@ -30,8 +30,13 @@ local function performRequest(requestParams)
end) end)
if success then if success then
Log.trace("Request {} success, status code {}", requestId, response.StatusCode) Log.trace("Request {} success, response {:#?}", requestId, response)
resolve(HttpResponse.fromRobloxResponse(response)) local httpResponse = HttpResponse.fromRobloxResponse(response)
if httpResponse:isSuccess() then
resolve(httpResponse)
else
reject(HttpError.fromResponse(httpResponse))
end
else else
Log.trace("Request {} failure: {:?}", requestId, response) Log.trace("Request {} failure: {:?}", requestId, response)
reject(HttpError.fromRobloxErrorString(response)) reject(HttpError.fromRobloxErrorString(response))
@@ -63,4 +68,4 @@ function Http.jsonDecode(source)
return HttpService:JSONDecode(source) return HttpService:JSONDecode(source)
end end
return Http return Http

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

Submodule plugin/modules/t deleted from f643b50682

View File

@@ -20,8 +20,8 @@ local function serializeFloat(value)
return value return value
end end
local ALL_AXES = {"X", "Y", "Z"} local ALL_AXES = { "X", "Y", "Z" }
local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"} local ALL_FACES = { "Right", "Top", "Back", "Left", "Bottom", "Front" }
local EncodedValue = {} local EncodedValue = {}
@@ -37,7 +37,10 @@ types = {
if ok then if ok then
output[key] = result output[key] = result
else else
local warning = ("Could not decode attribute value of type %q: %s"):format(typeof(value), tostring(result)) local warning = ("Could not decode attribute value of type %q: %s"):format(
typeof(value),
tostring(result)
)
warn(warning) warn(warning)
end end
end end
@@ -53,7 +56,10 @@ types = {
if ok then if ok then
output[key] = result output[key] = result
else else
local warning = ("Could not encode attribute value of type %q: %s"):format(typeof(value), tostring(result)) local warning = ("Could not encode attribute value of type %q: %s"):format(
typeof(value),
tostring(result)
)
warn(warning) warn(warning)
end end
end end
@@ -111,6 +117,7 @@ types = {
local pos = pod.position local pos = pod.position
local orient = pod.orientation local orient = pod.orientation
--stylua: ignore
return CFrame.new( return CFrame.new(
pos[1], pos[2], pos[3], pos[1], pos[2], pos[3],
orient[1][1], orient[1][2], orient[1][3], orient[1][1], orient[1][2], orient[1][3],
@@ -120,17 +127,14 @@ types = {
end, end,
toPod = function(roblox) toPod = function(roblox)
local x, y, z, local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = roblox:GetComponents()
r00, r01, r02,
r10, r11, r12,
r20, r21, r22 = roblox:GetComponents()
return { return {
position = {x, y, z}, position = { x, y, z },
orientation = { orientation = {
{r00, r01, r02}, { r00, r01, r02 },
{r10, r11, r12}, { r10, r11, r12 },
{r20, r21, r22}, { r20, r21, r22 },
}, },
} }
end, end,
@@ -140,7 +144,7 @@ types = {
fromPod = unpackDecoder(Color3.new), fromPod = unpackDecoder(Color3.new),
toPod = function(roblox) toPod = function(roblox)
return {roblox.r, roblox.g, roblox.b} return { roblox.r, roblox.g, roblox.b }
end, end,
}, },
@@ -161,10 +165,7 @@ types = {
local keypoints = {} local keypoints = {}
for index, keypoint in ipairs(pod.keypoints) do for index, keypoint in ipairs(pod.keypoints) do
keypoints[index] = ColorSequenceKeypoint.new( keypoints[index] = ColorSequenceKeypoint.new(keypoint.time, types.Color3.fromPod(keypoint.color))
keypoint.time,
types.Color3.fromPod(keypoint.color)
)
end end
return ColorSequence.new(keypoints) return ColorSequence.new(keypoints)
@@ -187,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,
}, },
@@ -204,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 = {}
@@ -238,6 +284,23 @@ types = {
toPod = serializeFloat, toPod = serializeFloat,
}, },
Font = {
fromPod = function(pod)
return Font.new(
pod.family,
if pod.weight ~= nil then Enum.FontWeight[pod.weight] else nil,
if pod.style ~= nil then Enum.FontStyle[pod.style] else nil
)
end,
toPod = function(roblox)
return {
family = roblox.Family,
weight = roblox.Weight.Name,
style = roblox.Style.Name,
}
end,
},
Int32 = { Int32 = {
fromPod = identity, fromPod = identity,
toPod = identity, toPod = identity,
@@ -248,11 +311,32 @@ types = {
toPod = identity, toPod = identity,
}, },
MaterialColors = {
fromPod = function(pod: { [string]: { number } })
local real = {}
for name, color in pod do
real[Enum.Material[name]] = Color3.fromRGB(color[1], color[2], color[3])
end
return real
end,
toPod = function(roblox: { [Enum.Material]: Color3 })
local pod = {}
for material, color in roblox do
pod[material.Name] = {
math.round(math.clamp(color.R, 0, 1) * 255),
math.round(math.clamp(color.G, 0, 1) * 255),
math.round(math.clamp(color.B, 0, 1) * 255),
}
end
return pod
end,
},
NumberRange = { NumberRange = {
fromPod = unpackDecoder(NumberRange.new), fromPod = unpackDecoder(NumberRange.new),
toPod = function(roblox) toPod = function(roblox)
return {roblox.Min, roblox.Max} return { roblox.Min, roblox.Max }
end, end,
}, },
@@ -261,11 +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( -- TODO: Add a test for NaN or Infinity values and envelopes
keypoint.time, -- Right now it isn't possible because it'd fail the roundtrip.
keypoint.value, -- It's more important that it works right now, though.
keypoint.envelope 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)
@@ -293,13 +378,26 @@ types = {
if pod == "Default" then if pod == "Default" then
return nil return nil
else else
return PhysicalProperties.new( -- Passing `nil` instead of not passing anything gives
pod.density, -- different results, so we have to branch here.
pod.friction, if pod.acousticAbsorption then
pod.elasticity, return (PhysicalProperties.new :: any)(
pod.frictionWeight, pod.density,
pod.elasticityWeight pod.friction,
) pod.elasticity,
pod.frictionWeight,
pod.elasticityWeight,
pod.acousticAbsorption
)
else
return PhysicalProperties.new(
pod.density,
pod.friction,
pod.elasticity,
pod.frictionWeight,
pod.elasticityWeight
)
end
end end
end, end,
@@ -313,6 +411,7 @@ types = {
elasticity = roblox.Elasticity, elasticity = roblox.Elasticity,
frictionWeight = roblox.FrictionWeight, frictionWeight = roblox.FrictionWeight,
elasticityWeight = roblox.ElasticityWeight, elasticityWeight = roblox.ElasticityWeight,
acousticAbsorption = roblox.AcousticAbsorption,
} }
end end
end, end,
@@ -320,10 +419,7 @@ types = {
Ray = { Ray = {
fromPod = function(pod) fromPod = function(pod)
return Ray.new( return Ray.new(types.Vector3.fromPod(pod.origin), types.Vector3.fromPod(pod.direction))
types.Vector3.fromPod(pod.origin),
types.Vector3.fromPod(pod.direction)
)
end, end,
toPod = function(roblox) toPod = function(roblox)
@@ -336,10 +432,7 @@ types = {
Rect = { Rect = {
fromPod = function(pod) fromPod = function(pod)
return Rect.new( return Rect.new(types.Vector2.fromPod(pod[1]), types.Vector2.fromPod(pod[2]))
types.Vector2.fromPod(pod[1]),
types.Vector2.fromPod(pod[2])
)
end, end,
toPod = function(roblox) toPod = function(roblox)
@@ -351,31 +444,28 @@ types = {
}, },
Ref = { Ref = {
fromPod = function(_pod) fromPod = function(_)
error("Ref cannot be decoded on its own") error("Ref cannot be decoded on its own")
end, end,
toPod = function(_roblox) toPod = function(_)
error("Ref can not be encoded on its own") error("Ref can not be encoded on its own")
end, end,
}, },
Region3 = { Region3 = {
fromPod = function(pod) fromPod = function(_)
error("Region3 is not implemented") error("Region3 is not implemented")
end, end,
toPod = function(roblox) toPod = function(_)
error("Region3 is not implemented") error("Region3 is not implemented")
end, end,
}, },
Region3int16 = { Region3int16 = {
fromPod = function(pod) fromPod = function(pod)
return Region3int16.new( return Region3int16.new(types.Vector3int16.fromPod(pod[1]), types.Vector3int16.fromPod(pod[2]))
types.Vector3int16.fromPod(pod[1]),
types.Vector3int16.fromPod(pod[2])
)
end, end,
toPod = function(roblox) toPod = function(roblox)
@@ -387,11 +477,11 @@ types = {
}, },
SharedString = { SharedString = {
fromPod = function(pod) fromPod = function(_pod)
error("SharedString is not supported") error("SharedString is not supported")
end, end,
toPod = function(roblox) toPod = function(_roblox)
error("SharedString is not supported") error("SharedString is not supported")
end, end,
}, },
@@ -405,16 +495,13 @@ types = {
fromPod = unpackDecoder(UDim.new), fromPod = unpackDecoder(UDim.new),
toPod = function(roblox) toPod = function(roblox)
return {roblox.Scale, roblox.Offset} return { roblox.Scale, roblox.Offset }
end, end,
}, },
UDim2 = { UDim2 = {
fromPod = function(pod) fromPod = function(pod)
return UDim2.new( return UDim2.new(types.UDim.fromPod(pod[1]), types.UDim.fromPod(pod[2]))
types.UDim.fromPod(pod[1]),
types.UDim.fromPod(pod[2])
)
end, end,
toPod = function(roblox) toPod = function(roblox)
@@ -445,7 +532,7 @@ types = {
fromPod = unpackDecoder(Vector2int16.new), fromPod = unpackDecoder(Vector2int16.new),
toPod = function(roblox) toPod = function(roblox)
return {roblox.X, roblox.Y} return { roblox.X, roblox.Y }
end, end,
}, },
@@ -465,14 +552,37 @@ types = {
fromPod = unpackDecoder(Vector3int16.new), fromPod = unpackDecoder(Vector3int16.new),
toPod = function(roblox) toPod = function(roblox)
return {roblox.X, roblox.Y, roblox.Z} return { roblox.X, roblox.Y, roblox.Z }
end, end,
}, },
} }
types.OptionalCFrame = {
fromPod = function(pod)
if pod == nil then
return nil
else
return types.CFrame.fromPod(pod)
end
end,
toPod = function(roblox)
if roblox == nil then
return nil
else
return types.CFrame.toPod(roblox)
end
end,
}
function EncodedValue.decode(encodedValue) function EncodedValue.decode(encodedValue)
local ty, value = next(encodedValue) local ty, value = next(encodedValue)
if ty == nil then
-- If the encoded pair is empty, assume it is an unoccupied optional value
return true, nil
end
local typeImpl = types[ty] local typeImpl = types[ty]
if typeImpl == nil then if typeImpl == nil then
return false, "Couldn't decode value " .. tostring(ty) return false, "Couldn't decode value " .. tostring(ty)

View File

@@ -1,72 +0,0 @@
return function()
local HttpService = game:GetService("HttpService")
local EncodedValue = require(script.Parent.EncodedValue)
local allValues = require(script.Parent.allValues)
local function deepEq(a, b)
if typeof(a) ~= typeof(b) then
return false
end
local ty = typeof(a)
if ty == "table" then
local visited = {}
for key, valueA in pairs(a) do
visited[key] = true
if not deepEq(valueA, b[key]) then
return false
end
end
for key, valueB in pairs(b) do
if visited[key] then
continue
end
if not deepEq(valueB, a[key]) then
return false
end
end
return true
else
return a == b
end
end
local extraAssertions = {
CFrame = function(value)
expect(value).to.equal(CFrame.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12))
end,
}
for testName, testEntry in pairs(allValues) do
it("round trip " .. testName, function()
local ok, decoded = EncodedValue.decode(testEntry.value)
assert(ok, decoded)
if extraAssertions[testName] ~= nil then
extraAssertions[testName](decoded)
end
local ok, encoded = EncodedValue.encode(decoded, testEntry.ty)
assert(ok, encoded)
if not deepEq(encoded, testEntry.value) then
local expected = HttpService:JSONEncode(testEntry.value)
local actual = HttpService:JSONEncode(encoded)
local message = string.format(
"Round-trip results did not match.\nExpected:\n%s\nActual:\n%s",
expected, actual
)
error(message)
end
end)
end
end

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",
} }
@@ -25,4 +26,4 @@ function Error:__tostring()
return ("Error(%s: %s)"):format(self.kind, tostring(self.extra)) return ("Error(%s: %s)"):format(self.kind, tostring(self.extra))
end end
return Error return Error

View File

@@ -53,6 +53,11 @@ function PropertyDescriptor:read(instance)
end end
if self.scriptability == "Custom" then if self.scriptability == "Custom" then
if customProperties[self.className] == nil then
local fullName = ("%s.%s"):format(instance.className, self.name)
return false, Error.new(Error.Kind.PropertyNotReadable, fullName)
end
local interface = customProperties[self.className][self.name] local interface = customProperties[self.className][self.name]
return interface.read(instance, self.name) return interface.read(instance, self.name)
@@ -79,6 +84,11 @@ function PropertyDescriptor:write(instance, value)
end end
if self.scriptability == "Custom" then if self.scriptability == "Custom" then
if customProperties[self.className] == nil then
local fullName = ("%s.%s"):format(instance.className, self.name)
return false, Error.new(Error.Kind.PropertyNotWritable, fullName)
end
local interface = customProperties[self.className][self.name] local interface = customProperties[self.className][self.name]
return interface.write(instance, self.name, value) return interface.write(instance, self.name, value)

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": [
@@ -207,6 +236,17 @@
}, },
"ty": "Float64" "ty": "Float64"
}, },
"Font": {
"value": {
"Font": {
"family": "rbxasset://fonts/families/SourceSansPro.json",
"weight": "Regular",
"style": "Normal",
"cachedFaceId": null
}
},
"ty": "Font"
},
"Int32": { "Int32": {
"value": { "value": {
"Int32": 6014 "Int32": 6014
@@ -219,6 +259,118 @@
}, },
"ty": "Int64" "ty": "Int64"
}, },
"MaterialColors": {
"value": {
"MaterialColors": {
"Grass": [
106,
127,
63
],
"Slate": [
63,
127,
107
],
"Concrete": [
127,
102,
63
],
"Brick": [
138,
86,
62
],
"Sand": [
143,
126,
95
],
"WoodPlanks": [
139,
109,
79
],
"Rock": [
102,
108,
111
],
"Glacier": [
101,
176,
234
],
"Snow": [
195,
199,
218
],
"Sandstone": [
137,
90,
71
],
"Mud": [
58,
46,
36
],
"Basalt": [
30,
30,
37
],
"Ground": [
102,
92,
59
],
"CrackedLava": [
232,
156,
74
],
"Asphalt": [
115,
123,
107
],
"Cobblestone": [
132,
123,
90
],
"Ice": [
129,
194,
224
],
"LeafyGrass": [
115,
132,
74
],
"Salt": [
198,
189,
181
],
"Limestone": [
206,
173,
148
],
"Pavement": [
148,
148,
140
]
}
},
"ty": "MaterialColors"
},
"NumberRange": { "NumberRange": {
"value": { "value": {
"NumberRange": [ "NumberRange": [
@@ -247,6 +399,41 @@
}, },
"ty": "NumberSequence" "ty": "NumberSequence"
}, },
"OptionalCFrame-None": {
"value": {
"OptionalCFrame": null
},
"ty": "OptionalCFrame"
},
"OptionalCFrame-Some": {
"value": {
"OptionalCFrame": {
"position": [
0.0,
0.0,
0.0
],
"orientation": [
[
1.0,
0.0,
0.0
],
[
0.0,
1.0,
0.0
],
[
0.0,
0.0,
1.0
]
]
}
},
"ty": "OptionalCFrame"
},
"PhysicalProperties-Custom": { "PhysicalProperties-Custom": {
"value": { "value": {
"PhysicalProperties": { "PhysicalProperties": {
@@ -254,7 +441,8 @@
"friction": 1.0, "friction": 1.0,
"elasticity": 0.0, "elasticity": 0.0,
"frictionWeight": 50.0, "frictionWeight": 50.0,
"elasticityWeight": 25.0 "elasticityWeight": 25.0,
"acousticAbsorption": 0.15625
} }
}, },
"ty": "PhysicalProperties" "ty": "PhysicalProperties"

View File

@@ -1,139 +1,10 @@
-- Thanks to Tiffany352 for this base64 implementation! local EncodingService = game:GetService("EncodingService")
local floor = math.floor
local char = string.char
local function encodeBase64(str)
local out = {}
local nOut = 0
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local strLen = #str
-- 3 octets become 4 hextets
for i = 1, strLen - 2, 3 do
local b1, b2, b3 = str:byte(i, i + 3)
local word = b3 + b2 * 256 + b1 * 256 * 256
local h4 = word % 64 + 1
word = floor(word / 64)
local h3 = word % 64 + 1
word = floor(word / 64)
local h2 = word % 64 + 1
word = floor(word / 64)
local h1 = word % 64 + 1
out[nOut + 1] = alphabet:sub(h1, h1)
out[nOut + 2] = alphabet:sub(h2, h2)
out[nOut + 3] = alphabet:sub(h3, h3)
out[nOut + 4] = alphabet:sub(h4, h4)
nOut = nOut + 4
end
local remainder = strLen % 3
if remainder == 2 then
-- 16 input bits -> 3 hextets (2 full, 1 partial)
local b1, b2 = str:byte(-2, -1)
-- partial is 4 bits long, leaving 2 bits of zero padding ->
-- offset = 4
local word = b2 * 4 + b1 * 4 * 256
local h3 = word % 64 + 1
word = floor(word / 64)
local h2 = word % 64 + 1
word = floor(word / 64)
local h1 = word % 64 + 1
out[nOut + 1] = alphabet:sub(h1, h1)
out[nOut + 2] = alphabet:sub(h2, h2)
out[nOut + 3] = alphabet:sub(h3, h3)
out[nOut + 4] = "="
elseif remainder == 1 then
-- 8 input bits -> 2 hextets (2 full, 1 partial)
local b1 = str:byte(-1, -1)
-- partial is 2 bits long, leaving 4 bits of zero padding ->
-- offset = 16
local word = b1 * 16
local h2 = word % 64 + 1
word = floor(word / 64)
local h1 = word % 64 + 1
out[nOut + 1] = alphabet:sub(h1, h1)
out[nOut + 2] = alphabet:sub(h2, h2)
out[nOut + 3] = "="
out[nOut + 4] = "="
end
-- if the remainder is 0, then no work is needed
return table.concat(out, "")
end
local function decodeBase64(str)
local out = {}
local nOut = 0
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local strLen = #str
local acc = 0
local nAcc = 0
local alphabetLut = {}
for i = 1, #alphabet do
alphabetLut[alphabet:sub(i, i)] = i - 1
end
-- 4 hextets become 3 octets
for i = 1, strLen do
local ch = str:sub(i, i)
local byte = alphabetLut[ch]
if byte then
acc = acc * 64 + byte
nAcc = nAcc + 1
end
if nAcc == 4 then
local b3 = acc % 256
acc = floor(acc / 256)
local b2 = acc % 256
acc = floor(acc / 256)
local b1 = acc % 256
out[nOut + 1] = char(b1)
out[nOut + 2] = char(b2)
out[nOut + 3] = char(b3)
nOut = nOut + 3
nAcc = 0
acc = 0
end
end
if nAcc == 3 then
-- 3 hextets -> 16 bit output
acc = acc * 64
acc = floor(acc / 256)
local b2 = acc % 256
acc = floor(acc / 256)
local b1 = acc % 256
out[nOut + 1] = char(b1)
out[nOut + 2] = char(b2)
elseif nAcc == 2 then
-- 2 hextets -> 8 bit output
acc = acc * 64
acc = floor(acc / 256)
acc = acc * 64
acc = floor(acc / 256)
local b1 = acc % 256
out[nOut + 1] = char(b1)
elseif nAcc == 1 then
error("Base64 has invalid length")
end
return table.concat(out, "")
end
return { return {
decode = decodeBase64, decode = function(input: string)
encode = encodeBase64, return buffer.tostring(EncodingService:Base64Decode(buffer.fromstring(input)))
} end,
encode = function(input: string)
return buffer.tostring(EncodingService:Base64Encode(buffer.fromstring(input)))
end,
}

View File

@@ -1,29 +0,0 @@
return function()
local base64 = require(script.Parent.base64)
it("should encode and decode", function()
local function try(str, expected)
local encoded = base64.encode(str)
expect(encoded).to.equal(expected)
expect(base64.decode(encoded)).to.equal(str)
end
try("Man", "TWFu")
try("Ma", "TWE=")
try("M", "TQ==")
try("ManM", "TWFuTQ==")
try(
[[Man is distinguished, not only by his reason, but by this ]]..
[[singular passion from other animals, which is a lust of the ]]..
[[mind, that by a perseverance of delight in the continued and ]]..
[[indefatigable generation of knowledge, exceeds the short ]]..
[[vehemence of any carnal pleasure.]],
[[TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sI]]..
[[GJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYW]]..
[[xzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJ]]..
[[zZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRl]]..
[[ZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZ]]..
[[SBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=]]
)
end)
end

View File

@@ -1,4 +1,47 @@
local CollectionService = game:GetService("CollectionService") local CollectionService = game:GetService("CollectionService")
local ScriptEditorService = game:GetService("ScriptEditorService")
local Error = require(script.Parent.Error)
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
local TERRAIN_MATERIAL_COLORS = {
Enum.Material.Grass,
Enum.Material.Slate,
Enum.Material.Concrete,
Enum.Material.Brick,
Enum.Material.Sand,
Enum.Material.WoodPlanks,
Enum.Material.Rock,
Enum.Material.Glacier,
Enum.Material.Snow,
Enum.Material.Sandstone,
Enum.Material.Mud,
Enum.Material.Basalt,
Enum.Material.Ground,
Enum.Material.CrackedLava,
Enum.Material.Asphalt,
Enum.Material.Cobblestone,
Enum.Material.Ice,
Enum.Material.LeafyGrass,
Enum.Material.Salt,
Enum.Material.Limestone,
Enum.Material.Pavement,
}
local function isAttributeNameValid(attributeName)
-- For SetAttribute to succeed, the attribute name must be less than or
-- equal to 100 characters...
return #attributeName <= 100
-- ...and must only contain alphanumeric characters, periods, hyphens,
-- underscores, or forward slashes.
and attributeName:match("[^%w%.%-_/]") == nil
end
local function isAttributeNameReserved(attributeName)
-- For SetAttribute to succeed, attribute names must not use the RBX
-- prefix, which is reserved by Roblox.
return attributeName:sub(1, 3) == "RBX"
end
-- Defines how to read and write properties that aren't directly scriptable. -- Defines how to read and write properties that aren't directly scriptable.
-- --
@@ -10,19 +53,45 @@ return {
return true, instance:GetAttributes() return true, instance:GetAttributes()
end, end,
write = function(instance, _, value) write = function(instance, _, value)
local existing = instance:GetAttributes() if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
for key, attr in pairs(value) do
instance:SetAttribute(key, attr)
end end
for key in pairs(existing) do local existing = instance:GetAttributes()
if value[key] == nil then local didAllWritesSucceed = true
instance:SetAttribute(key, nil)
for attributeName, attributeValue in pairs(value) do
if isAttributeNameReserved(attributeName) then
-- If the attribute name is reserved, then we don't
-- really care about reporting any failures about
-- it.
continue
end
if not isAttributeNameValid(attributeName) then
didAllWritesSucceed = false
continue
end
instance:SetAttribute(attributeName, attributeValue)
end
for existingAttributeName in pairs(existing) do
if isAttributeNameReserved(existingAttributeName) then
continue
end
if not isAttributeNameValid(existingAttributeName) then
didAllWritesSucceed = false
continue
end
if value[existingAttributeName] == nil then
instance:SetAttribute(existingAttributeName, nil)
end end
end end
return true return didAllWritesSucceed
end, end,
}, },
Tags = { Tags = {
@@ -52,13 +121,117 @@ return {
}, },
LocalizationTable = { LocalizationTable = {
Contents = { Contents = {
read = function(instance, key) read = function(instance, _)
return true, instance:GetContents() return true, instance:GetContents()
end, end,
write = function(instance, key, value) write = function(instance, _, value)
instance:SetContents(value) instance:SetContents(value)
return true return true
end, end,
}, },
}, },
Model = {
Scale = {
read = function(instance, _, _)
return true, instance:GetScale()
end,
write = function(instance, _, value)
return true, instance:ScaleTo(value)
end,
},
WorldPivotData = {
read = function(instance)
return true, instance.WorldPivot
end,
write = function(instance, _, value)
if value == nil then
return true, nil
else
instance.WorldPivot = value
return true
end
end,
},
},
Terrain = {
MaterialColors = {
read = function(instance: Terrain)
-- There's no way to get a list of every color, so we have to
-- make one.
local colors = {}
for _, material in TERRAIN_MATERIAL_COLORS do
colors[material] = instance:GetMaterialColor(material)
end
return true, colors
end,
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
end
for material, color in value do
instance:SetMaterialColor(material, color)
end
return true
end,
},
},
Script = {
Source = {
read = function(instance: Script)
return true, ScriptEditorService:GetEditorSource(instance)
end,
write = function(instance: Script, _, value: string)
task.spawn(function()
ScriptEditorService:UpdateSourceAsync(instance, function()
return value
end)
end)
return true
end,
},
},
ModuleScript = {
Source = {
read = function(instance: ModuleScript)
return true, ScriptEditorService:GetEditorSource(instance)
end,
write = function(instance: ModuleScript, _, value: string)
task.spawn(function()
ScriptEditorService:UpdateSourceAsync(instance, function()
return value
end)
end)
return true
end,
},
},
StyleRule = {
PropertiesSerialize = {
read = function(instance: StyleRule)
return true, instance:GetProperties()
end,
write = function(instance: StyleRule, _, value: { [any]: any })
if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
end
local existing = instance:GetProperties()
for itemName, itemValue in pairs(value) do
instance:SetProperty(itemName, itemValue)
end
for existingItemName in pairs(existing) do
if value[existingItemName] == nil then
instance:SetProperty(existingItemName, nil)
end
end
return true
end,
},
},
} }

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,8 @@ local function findCanonicalPropertyDescriptor(className, propertyName)
return PropertyDescriptor.fromRaw( return PropertyDescriptor.fromRaw(
currentClass.Properties[aliasData.AliasFor], currentClass.Properties[aliasData.AliasFor],
currentClassName, currentClassName,
aliasData.AliasFor) aliasData.AliasFor
)
end end
return nil return nil

View File

@@ -1,7 +0,0 @@
return function()
local RbxDom = require(script.Parent)
it("should load", function()
expect(RbxDom).to.be.ok()
end)
end

View File

@@ -1,19 +1,11 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TestEZ = require(ReplicatedStorage.TestEZ) local TestEZ = require(ReplicatedStorage.Packages:WaitForChild("TestEZ", 10))
local Rojo = ReplicatedStorage.Rojo local Rojo = ReplicatedStorage.Rojo
local DevSettings = require(Rojo.Plugin.DevSettings) local Settings = require(Rojo.Plugin.Settings)
Settings:set("logLevel", "Trace")
local setDevSettings = not DevSettings:hasChangedValues() Settings:set("typecheckingEnabled", true)
if setDevSettings then
DevSettings:createTestSettings()
end
require(Rojo.Plugin.runTests)(TestEZ) require(Rojo.Plugin.runTests)(TestEZ)
if setDevSettings then
DevSettings:resetValues()
end

View File

@@ -1,6 +1,8 @@
local Http = require(script.Parent.Parent.Http) local Packages = script.Parent.Parent.Packages
local Log = require(script.Parent.Parent.Log) local HttpService = game:GetService("HttpService")
local Promise = require(script.Parent.Parent.Promise) local Http = require(Packages.Http)
local Log = require(Packages.Log)
local Promise = require(Packages.Promise)
local Config = require(script.Parent.Config) local Config = require(script.Parent.Config)
local Types = require(script.Parent.Types) local Types = require(script.Parent.Types)
@@ -8,14 +10,9 @@ 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 validateApiSocketPacket = Types.ifEnabled(Types.ApiSocketPacket)
local validateApiSerialize = Types.ifEnabled(Types.ApiSerializeResponse)
--[[ local validateApiRefPatch = Types.ifEnabled(Types.ApiRefPatchResponse)
Returns a promise that will never resolve nor reject.
]]
local function hangingPromise()
return Promise.new(function() end)
end
local function rejectFailedRequests(response) local function rejectFailedRequests(response)
if response.code >= 400 then if response.code >= 400 then
@@ -30,15 +27,17 @@ end
local function rejectWrongProtocolVersion(infoResponseBody) local function rejectWrongProtocolVersion(infoResponseBody)
if infoResponseBody.protocolVersion ~= Config.protocolVersion then if infoResponseBody.protocolVersion ~= Config.protocolVersion then
local message = ( local message = (
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible." .. "Found a Rojo dev server, but it's using a different protocol version, and is incompatible."
"\nMake sure you have matching versions of both the Rojo plugin and server!" .. .. "\nMake sure you have matching versions of both the Rojo plugin and server!"
"\n\nYour client is version %s, with protocol version %s. It expects server version %s." .. .. "\n\nYour client is version %s, with protocol version %s. It expects server version %s."
"\nYour server is version %s, with protocol version %s." .. .. "\nYour server is version %s, with protocol version %s."
"\n\nGo to https://github.com/rojo-rbx/rojo for more details." .. "\n\nGo to https://github.com/rojo-rbx/rojo for more details."
):format( ):format(
Version.display(Config.version), Config.protocolVersion, Version.display(Config.version),
Config.protocolVersion,
Config.expectedServerVersionString, Config.expectedServerVersionString,
infoResponseBody.serverVersion, infoResponseBody.protocolVersion infoResponseBody.serverVersion,
infoResponseBody.protocolVersion
) )
return Promise.reject(message) return Promise.reject(message)
@@ -49,14 +48,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 = {}
@@ -65,14 +57,31 @@ local function rejectWrongPlaceId(infoResponseBody)
end end
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( ):format(game.PlaceId, table.concat(idList, "\n"))
tostring(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
@@ -85,13 +94,15 @@ local ApiContext = {}
ApiContext.__index = ApiContext ApiContext.__index = ApiContext
function ApiContext.new(baseUrl) function ApiContext.new(baseUrl)
assert(type(baseUrl) == "string") assert(type(baseUrl) == "string", "baseUrl must be a string")
local self = { local self = {
__baseUrl = baseUrl, __baseUrl = baseUrl,
__sessionId = nil, __sessionId = nil,
__messageCursor = -1, __messageCursor = -1,
__wsClient = nil,
__connected = true, __connected = true,
__activeRequests = {},
} }
return setmetatable(self, ApiContext) return setmetatable(self, ApiContext)
@@ -112,6 +123,17 @@ end
function ApiContext:disconnect() function ApiContext:disconnect()
self.__connected = false self.__connected = false
for request in self.__activeRequests do
Log.trace("Cancelling request {}", request)
request:cancel()
end
self.__activeRequests = {}
if self.__wsClient then
Log.trace("Closing WebSocket client")
self.__wsClient:Close()
end
self.__wsClient = nil
end end
function ApiContext:setMessageCursor(index) function ApiContext:setMessageCursor(index)
@@ -141,18 +163,15 @@ end
function ApiContext:read(ids) function ApiContext:read(ids)
local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ",")) local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ","))
return Http.get(url) return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
:andThen(rejectFailedRequests) if body.sessionId ~= self.__sessionId then
:andThen(Http.Response.json) return Promise.reject("Server changed ID")
:andThen(function(body) end
if body.sessionId ~= self.__sessionId then
return Promise.reject("Server changed ID")
end
assert(validateApiRead(body)) assert(validateApiRead(body))
return body return body
end) end)
end end
function ApiContext:write(patch) function ApiContext:write(patch)
@@ -189,63 +208,113 @@ function ApiContext:write(patch)
body = Http.jsonEncode(body) body = Http.jsonEncode(body)
return Http.post(url, body) return Http.post(url, body):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(responseBody)
:andThen(rejectFailedRequests) Log.info("Write response: {:?}", responseBody)
:andThen(Http.Response.json)
:andThen(function(body)
Log.info("Write response: {:?}", body)
return body return responseBody
end) end)
end end
function ApiContext:retrieveMessages() function ApiContext:connectWebSocket(packetHandlers)
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor) local url = ("%s/api/socket/%s"):format(self.__baseUrl, self.__messageCursor)
-- Convert HTTP/HTTPS URL to WS/WSS
url = url:gsub("^http://", "ws://"):gsub("^https://", "wss://")
local function sendRequest() return Promise.new(function(resolve, reject)
return Http.get(url) local success, wsClient =
:catch(function(err) pcall(HttpService.CreateWebStreamClient, HttpService, Enum.WebStreamClientType.WebSocket, {
if err.type == Http.Error.Kind.Timeout then Url = url,
if self.__connected then })
return sendRequest() if not success then
else reject("Failed to create WebSocket client: " .. tostring(wsClient))
return hangingPromise() return
end end
end self.__wsClient = wsClient
return Promise.reject(err) local closed, errored, received
end)
end
return sendRequest() received = self.__wsClient.MessageReceived:Connect(function(msg)
:andThen(rejectFailedRequests) local data = Http.jsonDecode(msg)
:andThen(Http.Response.json) if data.sessionId ~= self.__sessionId then
:andThen(function(body) Log.warn("Received message with wrong session ID; ignoring")
if body.sessionId ~= self.__sessionId then return
return Promise.reject("Server changed ID")
end end
assert(validateApiSubscribe(body)) assert(validateApiSocketPacket(data))
self:setMessageCursor(body.messageCursor) Log.trace("Received websocket packet: {:#?}", data)
return body.messages local handler = packetHandlers[data.packetType]
if handler then
local ok, err = pcall(handler, data.body)
if not ok then
Log.error("Error in WebSocket packet handler for type '%s': %s", data.packetType, err)
end
else
Log.warn("No handler for WebSocket packet type '%s'", data.packetType)
end
end) end)
closed = self.__wsClient.Closed:Connect(function()
closed:Disconnect()
errored:Disconnect()
received:Disconnect()
if self.__connected then
reject("WebSocket connection closed unexpectedly")
else
resolve()
end
end)
errored = self.__wsClient.Error:Connect(function(code, msg)
closed:Disconnect()
errored:Disconnect()
received:Disconnect()
reject("WebSocket error: " .. code .. " - " .. msg)
end)
end)
end end
function ApiContext:open(id) function ApiContext:open(id)
local url = ("%s/api/open/%s"):format(self.__baseUrl, id) local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
return Http.post(url, "") return Http.post(url, ""):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
:andThen(rejectFailedRequests) if body.sessionId ~= self.__sessionId then
:andThen(Http.Response.json) return Promise.reject("Server changed ID")
:andThen(function(body) end
if body.sessionId ~= self.__sessionId then
return Promise.reject("Server changed ID")
end
return nil return nil
end) end)
end end
return ApiContext 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

View File

@@ -1,7 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
@@ -23,8 +24,10 @@ local function BorderedContainer(props)
layoutOrder = props.layoutOrder, layoutOrder = props.layoutOrder,
}, { }, {
Content = e("Frame", { Content = e("Frame", {
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, -2, 1, -2),
Position = UDim2.new(0, 1, 0, 1),
BackgroundTransparency = 1, BackgroundTransparency = 1,
ZIndex = 2,
}, props[Roact.Children]), }, props[Roact.Children]),
Border = e(SlicedImage, { Border = e(SlicedImage, {
@@ -38,4 +41,4 @@ local function BorderedContainer(props)
end) end)
end end
return BorderedContainer return BorderedContainer

View File

@@ -1,14 +1,16 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Flipper = require(Rojo.Flipper) 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 SlicedImage = require(script.Parent.SlicedImage) local SlicedImage = require(script.Parent.SlicedImage)
local Tooltip = require(script.Parent.Tooltip)
local e = Roact.createElement local e = Roact.createElement
@@ -21,18 +23,16 @@ end
function Checkbox:didUpdate(lastProps) function Checkbox:didUpdate(lastProps)
if lastProps.active ~= self.props.active then if lastProps.active ~= self.props.active then
self.motor:setGoal( self.motor:setGoal(Flipper.Spring.new(self.props.active and 1 or 0, {
Flipper.Spring.new(self.props.active and 1 or 0, { frequency = 6,
frequency = 6, dampingRatio = 1.1,
dampingRatio = 1.1, }))
})
)
end end
end 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)
@@ -49,18 +49,29 @@ function Checkbox:render()
ZIndex = self.props.zIndex, ZIndex = self.props.zIndex,
BackgroundTransparency = 1, BackgroundTransparency = 1,
[Roact.Event.Activated] = self.props.onClick, [Roact.Event.Activated] = function()
if self.props.locked then
return
end
self.props.onClick()
end,
}, { }, {
StateTip = e(Tooltip.Trigger, {
text = (if self.props.locked
then (self.props.lockedTooltip or "(Cannot be changed right now)") .. "\n"
else "") .. (if self.props.active then "Enabled" else "Disabled"),
}),
Active = e(SlicedImage, { 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 = 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),
@@ -73,13 +84,15 @@ 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),
}, { }, {
Icon = e("ImageLabel", { Icon = e("ImageLabel", {
Image = Assets.Images.Checkbox.Inactive, Image = if self.props.locked
ImageColor3 = theme.Inactive.IconColor, then Assets.Images.Checkbox.Locked
else Assets.Images.Checkbox.Inactive,
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),
@@ -93,4 +106,4 @@ function Checkbox:render()
end) end)
end end
return Checkbox return Checkbox

View File

@@ -0,0 +1,160 @@
local StudioService = game:GetService("StudioService")
local AssetService = game:GetService("AssetService")
type CachedImageInfo = {
pixels: buffer,
size: Vector2,
}
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local e = Roact.createElement
local EditableImage = require(Plugin.App.Components.EditableImage)
local imageCache: { [string]: CachedImageInfo } = {}
local function cloneBuffer(b: buffer): buffer
local newBuffer = buffer.create(buffer.len(b))
buffer.copy(newBuffer, 0, b)
return newBuffer
end
local function getImageSizeAndPixels(image: string): (Vector2, buffer)
local cachedImage = imageCache[image]
if not cachedImage then
local editableImage = AssetService:CreateEditableImageAsync(Content.fromUri(image))
local size = editableImage.Size
local pixels = editableImage:ReadPixelsBuffer(Vector2.zero, size)
imageCache[image] = {
pixels = pixels,
size = size,
}
return size, cloneBuffer(pixels)
end
return cachedImage.size, cloneBuffer(cachedImage.pixels)
end
local function getRecoloredClassIcon(className, color)
local iconProps = StudioService:GetClassIcon(className)
if iconProps and color then
--stylua: ignore
local success, editableImageSize, editableImagePixels = pcall(function(_iconProps: { [any]: any }, _color: Color3): (Vector2, buffer)
local size, pixels = getImageSizeAndPixels(_iconProps.Image)
local pixelsLen = buffer.len(pixels)
local minVal, maxVal = math.huge, -math.huge
for i = 0, pixelsLen, 4 do
if buffer.readu8(pixels, i + 3) == 0 then
continue
end
local pixelVal = math.max(
buffer.readu8(pixels, i),
buffer.readu8(pixels, i + 1),
buffer.readu8(pixels, i + 2)
)
minVal = math.min(minVal, pixelVal)
maxVal = math.max(maxVal, pixelVal)
end
local hue, sat, val = _color:ToHSV()
for i = 0, pixelsLen, 4 do
if buffer.readu8(pixels, i + 3) == 0 then
continue
end
local gIndex = i + 1
local bIndex = i + 2
local pixelVal = math.max(
buffer.readu8(pixels, i),
buffer.readu8(pixels, gIndex),
buffer.readu8(pixels, bIndex)
)
local newVal = val
if minVal < maxVal then
-- Remap minVal - maxVal to val*0.9 - val
newVal = val * (0.9 + 0.1 * (pixelVal - minVal) / (maxVal - minVal))
end
local newPixelColor = Color3.fromHSV(hue, sat, newVal)
buffer.writeu8(pixels, i, newPixelColor.R)
buffer.writeu8(pixels, gIndex, newPixelColor.G)
buffer.writeu8(pixels, bIndex, newPixelColor.B)
end
return size, pixels
end, iconProps, color)
if success then
iconProps.EditableImagePixels = editableImagePixels
iconProps.EditableImageSize = editableImageSize
end
end
return iconProps
end
local ClassIcon = Roact.PureComponent:extend("ClassIcon")
function ClassIcon:init()
self.state = {
iconProps = nil,
}
end
function ClassIcon:updateIcon()
local props = self.props
local iconProps = getRecoloredClassIcon(props.className, props.color)
self:setState({
iconProps = iconProps,
})
end
function ClassIcon:didMount()
self:updateIcon()
end
function ClassIcon:didUpdate(lastProps)
if lastProps.className ~= self.props.className or lastProps.color ~= self.props.color then
self:updateIcon()
end
end
function ClassIcon:render()
local iconProps = self.state.iconProps
if not iconProps then
return nil
end
return e(
"ImageLabel",
{
Size = self.props.size,
Position = self.props.position,
LayoutOrder = self.props.layoutOrder,
AnchorPoint = self.props.anchorPoint,
ImageTransparency = self.props.transparency,
Image = iconProps.Image,
ImageRectOffset = iconProps.ImageRectOffset,
ImageRectSize = iconProps.ImageRectSize,
BackgroundTransparency = 1,
},
if iconProps.EditableImagePixels
then e(EditableImage, {
size = iconProps.EditableImageSize,
pixels = iconProps.EditableImagePixels,
})
else nil
)
end
return ClassIcon

View File

@@ -0,0 +1,182 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Flipper = require(Packages.Flipper)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme)
local bindingUtil = require(Plugin.App.bindingUtil)
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
local SlicedImage = require(script.Parent.SlicedImage)
local ScrollingFrame = require(script.Parent.ScrollingFrame)
local Tooltip = require(script.Parent.Tooltip)
local e = Roact.createElement
local Dropdown = Roact.Component:extend("Dropdown")
function Dropdown:init()
self.openMotor = Flipper.SingleMotor.new(0)
self.openBinding = bindingUtil.fromMotor(self.openMotor)
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
self:setState({
open = false,
})
end
function Dropdown:didUpdate(prevProps)
if self.props.locked and not prevProps.locked then
self:setState({
open = false,
})
end
self.openMotor:setGoal(Flipper.Spring.new(self.state.open and 1 or 0, {
frequency = 6,
dampingRatio = 1.1,
}))
end
function Dropdown:render()
return Theme.with(function(theme)
local dropdownTheme = theme.Dropdown
local optionButtons = {}
local width = -1
for i, option in self.props.options do
local text = tostring(option or "")
local textBounds = getTextBoundsAsync(text, theme.Font.Main, theme.TextSize.Body, math.huge)
if textBounds.X > width then
width = textBounds.X
end
optionButtons[text] = e("TextButton", {
Text = text,
LayoutOrder = i,
Size = UDim2.new(1, 0, 0, 24),
BackgroundColor3 = dropdownTheme.BackgroundColor,
TextTransparency = self.props.transparency,
BackgroundTransparency = self.props.transparency,
BorderSizePixel = 0,
TextColor3 = dropdownTheme.TextColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextSize = theme.TextSize.Body,
FontFace = theme.Font.Main,
[Roact.Event.Activated] = function()
if self.props.locked then
return
end
self:setState({
open = false,
})
self.props.onClick(option)
end,
}, {
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 6),
}),
})
end
return e("ImageButton", {
Size = UDim2.new(0, width + 50, 0, 28),
Position = self.props.position,
AnchorPoint = self.props.anchorPoint,
LayoutOrder = self.props.layoutOrder,
ZIndex = self.props.zIndex,
BackgroundTransparency = 1,
[Roact.Event.Activated] = function()
if self.props.locked then
return
end
self:setState({
open = not self.state.open,
})
end,
}, {
Border = e(SlicedImage, {
slice = Assets.Slices.RoundedBorder,
color = dropdownTheme.BorderColor,
transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0),
}, {
DropArrow = e("ImageLabel", {
Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow,
ImageColor3 = dropdownTheme.IconColor,
ImageTransparency = self.props.transparency,
Size = UDim2.new(0, 18, 0, 18),
Position = UDim2.new(1, -6, 0.5, 0),
AnchorPoint = Vector2.new(1, 0.5),
Rotation = self.openBinding:map(function(a)
return a * 180
end),
BackgroundTransparency = 1,
}, {
StateTip = if self.props.locked
then e(Tooltip.Trigger, {
text = self.props.lockedTooltip or "(Cannot be changed right now)",
})
else nil,
}),
Active = e("TextLabel", {
Size = UDim2.new(1, -30, 1, 0),
Position = UDim2.new(0, 6, 0, 0),
BackgroundTransparency = 1,
Text = self.props.active,
FontFace = theme.Font.Main,
TextSize = theme.TextSize.Body,
TextColor3 = dropdownTheme.TextColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency,
}),
}),
Options = if self.state.open
then e(SlicedImage, {
slice = Assets.Slices.RoundedBackground,
color = dropdownTheme.BackgroundColor,
position = UDim2.new(1, 0, 1, 3),
size = self.openBinding:map(function(a)
return UDim2.new(1, 0, a * math.min(3, #self.props.options), 0)
end),
anchorPoint = Vector2.new(1, 0),
}, {
Border = e(SlicedImage, {
slice = Assets.Slices.RoundedBorder,
color = dropdownTheme.BorderColor,
transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0),
}),
ScrollingFrame = e(ScrollingFrame, {
size = UDim2.new(1, -4, 1, -4),
position = UDim2.new(0, 2, 0, 2),
transparency = self.props.transparency,
contentSize = self.contentSize,
}, {
Layout = e("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Top,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 0),
[Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize)
end,
}),
Options = Roact.createFragment(optionButtons),
}),
})
else nil,
})
end)
end
return Dropdown

View File

@@ -0,0 +1,42 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local e = Roact.createElement
local EditableImage = Roact.PureComponent:extend("EditableImage")
function EditableImage:init()
self.ref = Roact.createRef()
end
function EditableImage:writePixels()
local image = self.ref.current :: EditableImage
if not image then
return
end
if not self.props.pixels then
return
end
image:WritePixelsBuffer(Vector2.zero, self.props.size, self.props.pixels)
end
function EditableImage:render()
return e("EditableImage", {
Size = self.props.size,
[Roact.Ref] = self.ref,
})
end
function EditableImage:didMount()
self:writePixels()
end
function EditableImage:didUpdate()
self:writePixels()
end
return EditableImage

View File

@@ -1,15 +1,78 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) 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", {
@@ -28,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", {
@@ -52,4 +106,4 @@ local function Header(props)
end) end)
end end
return Header return Header

View File

@@ -1,8 +1,9 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Flipper = require(Rojo.Flipper) local Flipper = require(Packages.Flipper)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local bindingUtil = require(Plugin.App.bindingUtil) local bindingUtil = require(Plugin.App.bindingUtil)
@@ -29,6 +30,7 @@ function IconButton:render()
Position = self.props.position, Position = self.props.position,
AnchorPoint = self.props.anchorPoint, AnchorPoint = self.props.anchorPoint,
Visible = self.props.visible,
LayoutOrder = self.props.layoutOrder, LayoutOrder = self.props.layoutOrder,
ZIndex = self.props.zIndex, ZIndex = self.props.zIndex,
BackgroundTransparency = 1, BackgroundTransparency = 1,
@@ -36,15 +38,11 @@ function IconButton:render()
[Roact.Event.Activated] = self.props.onClick, [Roact.Event.Activated] = self.props.onClick,
[Roact.Event.MouseEnter] = function() [Roact.Event.MouseEnter] = function()
self.motor:setGoal( self.motor:setGoal(Flipper.Spring.new(1, HOVER_SPRING_PROPS))
Flipper.Spring.new(1, HOVER_SPRING_PROPS)
)
end, end,
[Roact.Event.MouseLeave] = function() [Roact.Event.MouseLeave] = function()
self.motor:setGoal( self.motor:setGoal(Flipper.Spring.new(0, HOVER_SPRING_PROPS))
Flipper.Spring.new(0, HOVER_SPRING_PROPS)
)
end, end,
}, { }, {
Icon = e("ImageLabel", { Icon = e("ImageLabel", {
@@ -73,7 +71,9 @@ function IconButton:render()
BackgroundTransparency = 1, BackgroundTransparency = 1,
}), }),
Children = Roact.createFragment(self.props[Roact.Children]),
}) })
end end
return IconButton return IconButton

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

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