Compare commits

...

64 Commits

Author SHA1 Message Date
Jaguar
d147ae7bb6 Fix typos (#239) 2019-08-28 21:28:52 -07:00
Lucien Greathouse
9b6ce57c79 Add notice about 0.4.x end-of-life. Closes #235. 2019-08-27 14:46:29 -07:00
Lucien Greathouse
ed85d2a327 Fix doc image links 2019-02-08 14:08:59 -08:00
Lucien Greathouse
7e5b8cadfc Update doc links 2019-01-28 15:39:52 -08:00
Lucien Greathouse
c805f4fa2d Point site URL to 0.4.x 2019-01-12 16:33:08 -08:00
Lucien Greathouse
f09d7fb0ff Update docs home to be more specific 2019-01-10 10:51:03 -08:00
Lucien Greathouse
4e918bab5e Ignore generate-docs command, should only exist on master 2019-01-09 23:20:19 -08:00
Lucien Greathouse
8e3f9b3bfd Add links to newer docs from 0.4.x docs 2019-01-09 23:16:08 -08:00
Lucien Greathouse
300975f49c 0.4.13 2018-11-12 21:51:48 -08:00
Lucien Greathouse
325cf56457 Update CHANGES and test-project 2018-11-12 21:47:08 -08:00
Lucien Greathouse
71b1320aa8 Issue a warning instead of dying when a partition target does not exist 2018-11-12 21:45:28 -08:00
Lucien Greathouse
c5abc87dbd Increase polling interval to 300ms 2018-08-17 11:24:05 -07:00
Lucien Greathouse
bd7ab593d5 Release 0.4.12 2018-06-21 11:21:42 -07:00
Lucien Greathouse
c93da3f6b2 Update CHANGES 2018-06-21 11:19:22 -07:00
Lucien Greathouse
8b90e98696 Added a plugin action for the sync in command (#80) 2018-06-21 11:17:26 -07:00
Lucien Greathouse
bc40ec8a5a Update CHANGES 2018-06-21 11:11:03 -07:00
Lucien Greathouse
f19cbccdd5 Fix assertion failure when renaming files.
Fixes #78.
2018-06-21 11:10:28 -07:00
Lucien Greathouse
f25ae914e4 Add TODO about preserving partition roots in reconciliation 2018-06-20 18:23:24 -07:00
Lucien Greathouse
fb7bfa928a Release 0.4.11 2018-06-10 15:54:57 -07:00
Lucien Greathouse
100d69262c Update CHANGES 2018-06-10 15:52:42 -07:00
Lucien Greathouse
5e01658846 Remove straggling debug message 2018-06-10 15:50:30 -07:00
Lucien Greathouse
ccec93aee8 Untangle route terminology a bit 2018-06-10 15:50:03 -07:00
Lucien Greathouse
a089d82023 Fix incorrect route being assigned to init.lua and init.model.json files 2018-06-10 15:44:56 -07:00
Lucien Greathouse
82ba583fa0 Fix incorrect synchronization for Plugin:_pull that would make polling flaky 2018-06-10 15:13:49 -07:00
Lucien Greathouse
1b82044d7d Defensively insert existing instances into RouteMap 2018-06-10 15:03:36 -07:00
Lucien Greathouse
0d49a2e0af Mention VS Code extension in getting started guide 2018-06-02 01:04:31 -07:00
Lucien Greathouse
1343d3a2a9 Pick up rest of changes for 0.4.10, oops 2018-06-02 00:50:35 -07:00
Lucien Greathouse
a86001b85c Release 0.4.10 2018-06-01 23:51:35 -07:00
Lucien Greathouse
d6dd46c467 Fix JsonModelPlugin marking paths as changed correctly 2018-06-01 23:38:49 -07:00
Lucien Greathouse
320974074c Update docs 2018-06-01 23:33:36 -07:00
Lucien Greathouse
7b824abe52 Update CHANGES 2018-06-01 23:30:59 -07:00
Lucien Greathouse
bfd33f4b8d Support init.model.json
Closes #66.
2018-06-01 23:29:39 -07:00
Lucien Greathouse
d5a21a0513 Update plugin .luacheckrc to be more strict 2018-06-01 23:11:58 -07:00
Lucien Greathouse
c894b38f06 Improve plugin API robustness 2018-06-01 23:11:50 -07:00
Lucien Greathouse
a86347ea32 Add typechecks to reconciler and improve robustness a touch 2018-06-01 22:34:11 -07:00
Lucien Greathouse
b60bfc7495 Make nil checks more robust.
This represents an evolution in how I've been thinking about Lua -- using boolean coercion
is generally a bad idea I think because it obscures the underlying types.

It also makes it so that if a boolean is eronneously passed into a function, and it
happens to be a 'false' value, it will be coerced into the nil case instead of being
reported as an error, no matter how unintuitive the resulting error might be.
2018-06-01 22:21:59 -07:00
Lucien Greathouse
4b2f27b26d Fix error when targeting invalid services 2018-06-01 22:17:54 -07:00
Lucien Greathouse
f4d7dda8e3 Make docs on JSON model versioning more explicit 2018-05-26 17:19:37 -07:00
Lucien Greathouse
0d6e3e66ce Release 0.4.9 2018-05-26 17:02:04 -07:00
Lucien Greathouse
7e4d451765 Update Sync Details docs 2018-05-26 17:00:23 -07:00
Lucien Greathouse
804bbc93b7 Make JSON models less strict 2018-05-26 16:59:09 -07:00
Lucien Greathouse
e7fe4ac3ec Remove vestigial backwards syncing functionality.
This functionality won't be present until the refactor in 0.5.0
2018-05-26 16:44:25 -07:00
Lucien Greathouse
40c41b4400 Update Sync Details docs to mention how JSON models work.
Closes #71.
2018-05-26 16:41:38 -07:00
Lucien Greathouse
0936c7c97d Fix indentation in CHANGES 2018-05-26 16:23:13 -07:00
Lucien Greathouse
9ac537d38f Add entry to CHANGES 2018-05-26 16:23:09 -07:00
Lucien Greathouse
fcfd55ff76 Fix error in RouteMap
Closes #72.
2018-05-26 16:19:58 -07:00
Lucien Greathouse
c2495ed57f Release 0.4.8 (oops) 2018-05-25 23:42:31 -07:00
Lucien Greathouse
6ad763fc01 Fix flip-flopped arguments in RouteMap:_removeInternal 2018-05-25 23:40:34 -07:00
Lucien Greathouse
c856a3e361 Release 0.4.7 2018-05-25 23:31:01 -07:00
Lucien Greathouse
aa5f0cc335 Issue a warning if no partitions are specified during serve.
Closes #40
2018-05-22 11:04:53 -07:00
Lucien Greathouse
b067335bbf Update CHANGES 2018-05-22 10:55:23 -07:00
Jonathan Holmes
7d24a14004 Added plugin icons to Rojo (#70) 2018-05-22 10:52:55 -07:00
Lucien Greathouse
910be640e9 Release 0.4.6 2018-05-21 13:26:25 -07:00
Lucien Greathouse
3137753afa Update CHANGES 2018-05-21 13:09:00 -07:00
Lucien Greathouse
000ff351a5 Improve plugin handling with regards to restarts and UI
Closes #67.
2018-05-21 13:05:52 -07:00
Lucien Greathouse
533c8ddaf7 Update CHANGES 2018-05-21 12:55:41 -07:00
Lucien Greathouse
f777d1b6c6 Update CHANGES 2018-05-21 12:52:46 -07:00
Lucien Greathouse
8b17d3b7d9 Intense robustness pass 2018-05-21 12:48:25 -07:00
Lucien Greathouse
6fbe1daf8e Add folder for testing script duplication bug 2018-05-21 12:47:51 -07:00
Lucien Greathouse
3bd191414b Update CHANGES 2018-05-21 11:45:55 -07:00
Lucien Greathouse
fd2cb3495b Make reconciler more robust with regards to RouteMap 2018-05-21 11:21:31 -07:00
Lucien Greathouse
e9d33bdc02 Skip reparenting if parent is the same 2018-05-21 11:16:56 -07:00
Lucien Greathouse
c0f4b31ab3 Make sure all descendants get removed from the RouteMap 2018-05-21 10:40:06 -07:00
Lucien Greathouse
78de30dcf2 Fix missing colon in plugin stopped polling message 2018-05-01 14:36:25 -07:00
32 changed files with 398 additions and 206 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/site /site
/generate-docs

View File

@@ -1,7 +1,44 @@
# Rojo Change Log # Rojo Change Log
## Current Master ## Current master
*No changes* * *No changes*
## 0.4.13 (November 12, 2018)
* When `rojo.json` points to a file or directory that does not exist, Rojo now issues a warning instead of throwing an error and exiting
## 0.4.12 (June 21, 2018)
* Fixed obscure assertion failure when renaming or deleting files ([#78](https://github.com/LPGhatguy/rojo/issues/78))
* Added a `PluginAction` for the sync in command, which should help with some automation scripts ([#80](https://github.com/LPGhatguy/rojo/pull/80))
## 0.4.11 (June 10, 2018)
* Defensively insert existing instances into RouteMap; should fix most duplication cases when syncing into existing trees.
* Fixed incorrect synchronization from `Plugin:_pull` that would cause polling to create issues
* Fixed incorrect file routes being assigned to `init.lua` and `init.model.json` files
* Untangled route handling-internals slightly
## 0.4.10 (June 2, 2018)
* Added support for `init.model.json` files, which enable versioning `Tool` instances (among other things) with Rojo. ([#66](https://github.com/LPGhatguy/rojo/issues/66))
* Fixed obscure error when syncing into an invalid service.
* Fixed multiple sync processes occurring when a server ID mismatch is detected.
## 0.4.9 (May 26, 2018)
* Fixed warning when renaming or removing files that would sometimes corrupt the instance cache ([#72](https://github.com/LPGhatguy/rojo/pull/72))
* JSON models are no longer as strict -- `Children` and `Properties` are now optional.
## 0.4.8 (May 26, 2018)
* Hotfix to prevent errors from being thrown when objects managed by Rojo are deleted
## 0.4.7 (May 25, 2018)
* Added icons to the Rojo plugin, made by [@Vorlias](https://github.com/Vorlias)! ([#70](https://github.com/LPGhatguy/rojo/pull/70))
* Server will now issue a warning if no partitions are specified in `rojo serve` ([#40](https://github.com/LPGhatguy/rojo/issues/40))
## 0.4.6 (May 21, 2018)
* Rojo handles being restarted by Roblox Studio more gracefully ([#67](https://github.com/LPGhatguy/rojo/issues/67))
* Folders should no longer get collapsed when syncing occurs.
* **Significant** robustness improvements with regards to caching.
* **This should catch all existing script duplication bugs.**
* If there are any bugs with script duplication or caching in the future, restarting the Rojo server process will fix them for that session.
* Fixed message in plugin not being prefixed with `Rojo: `.
## 0.4.5 (May 1, 2018) ## 0.4.5 (May 1, 2018)
* Rojo messages are now prefixed with `Rojo: ` to make them stand out in the output more. * Rojo messages are now prefixed with `Rojo: ` to make them stand out in the output more.
@@ -17,11 +54,11 @@
## 0.4.3 (April 7, 2018) ## 0.4.3 (April 7, 2018)
* Plugin now automatically selects `HttpService` if it determines that HTTP isn't enabled ([#58](https://github.com/LPGhatguy/rojo/pull/58)) * Plugin now automatically selects `HttpService` if it determines that HTTP isn't enabled ([#58](https://github.com/LPGhatguy/rojo/pull/58))
* Plugin now has much more robust handling and will wipe all state when the server changes. * Plugin now has much more robust handling and will wipe all state when the server changes.
* This should fix issues that would otherwise be solved by restarting Roblox Studio. * This should fix issues that would otherwise be solved by restarting Roblox Studio.
## 0.4.2 (April 4, 2018) ## 0.4.2 (April 4, 2018)
* Fixed final case of duplicated instance insertion, caused by reconciled instances not being inserted into `RouteMap`. * Fixed final case of duplicated instance insertion, caused by reconciled instances not being inserted into `RouteMap`.
* The reconciler is still not a perfect solution, especially if script instances get moved around without being destroyed. I don't think this can be fixed before a big refactor. * The reconciler is still not a perfect solution, especially if script instances get moved around without being destroyed. I don't think this can be fixed before a big refactor.
## 0.4.1 (April 1, 2018) ## 0.4.1 (April 1, 2018)
* Merged plugin repository into main Rojo repository for easier tracking. * Merged plugin repository into main Rojo repository for easier tracking.
@@ -29,9 +66,9 @@
## 0.4.0 (March 27, 2018) ## 0.4.0 (March 27, 2018)
* Protocol version 1, which shifts more responsibility onto the server * Protocol version 1, which shifts more responsibility onto the server
* This is a **major breaking** change! * This is a **major breaking** change!
* The server now has a content of 'filter plugins', which transform data at various stages in the pipeline * The server now has a content of 'filter plugins', which transform data at various stages in the pipeline
* The server now exposes Roblox instance objects instead of file contents, which lines up with how `rojo pack` will work, and paves the way for more robust syncing. * The server now exposes Roblox instance objects instead of file contents, which lines up with how `rojo pack` will work, and paves the way for more robust syncing.
* Added `*.model.json` files, which let you embed small Roblox objects into your Rojo tree. * Added `*.model.json` files, which let you embed small Roblox objects into your Rojo tree.
* Improved error messages in some cases ([#46](https://github.com/LPGhatguy/rojo/issues/46)) * Improved error messages in some cases ([#46](https://github.com/LPGhatguy/rojo/issues/46))
@@ -41,18 +78,18 @@
## 0.3.1 (December 14, 2017) ## 0.3.1 (December 14, 2017)
* Improved error reporting when invalid JSON is found in a `rojo.json` project * Improved error reporting when invalid JSON is found in a `rojo.json` project
* These messages are passed on from Serde * These messages are passed on from Serde
## 0.3.0 (December 12, 2017) ## 0.3.0 (December 12, 2017)
* Factored out the plugin into a separate repository * Factored out the plugin into a separate repository
* Fixed server when using a file as a partition * Fixed server when using a file as a partition
* Previously, trailing slashes were put on the end of a partition even if the read request was an empty string. This broke file reading on Windows when a partition pointed to a file instead of a directory! * Previously, trailing slashes were put on the end of a partition even if the read request was an empty string. This broke file reading on Windows when a partition pointed to a file instead of a directory!
* Started running automatic tests on Travis CI (#9) * Started running automatic tests on Travis CI (#9)
## 0.2.3 (December 4, 2017) ## 0.2.3 (December 4, 2017)
* Plugin only release * Plugin only release
* Tightened `init` file rules to only match script files * Tightened `init` file rules to only match script files
* Previously, Rojo would sometimes pick up the wrong file when syncing * Previously, Rojo would sometimes pick up the wrong file when syncing
## 0.2.2 (December 1, 2017) ## 0.2.2 (December 1, 2017)
* Plugin only release * Plugin only release

View File

@@ -8,7 +8,7 @@
<a href="https://travis-ci.org/LPGhatguy/rojo"> <a href="https://travis-ci.org/LPGhatguy/rojo">
<img src="https://api.travis-ci.org/LPGhatguy/rojo.svg?branch=master" alt="Travis-CI Build Status" /> <img src="https://api.travis-ci.org/LPGhatguy/rojo.svg?branch=master" alt="Travis-CI Build Status" />
</a> </a>
<img src="https://img.shields.io/badge/latest_version-0.4.5-brightgreen.svg" alt="Current server version" /> <img src="https://img.shields.io/badge/latest_version-0.4.13-brightgreen.svg" alt="Current server version" />
<a href="https://lpghatguy.github.io/rojo"> <a href="https://lpghatguy.github.io/rojo">
<img src="https://img.shields.io/badge/documentation-website-brightgreen.svg" alt="Rojo Documentation" /> <img src="https://img.shields.io/badge/documentation-website-brightgreen.svg" alt="Rojo Documentation" />
</a> </a>

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

BIN
assets/rojo-sync-in.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

BIN
assets/rojo-test-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 B

View File

@@ -56,7 +56,7 @@ If your project is in the right place, Rojo will let you know that it was found
In Roblox Studio, open the plugins tab and find Rojo's buttons. In Roblox Studio, open the plugins tab and find Rojo's buttons.
![Location of Rojo's plugin buttons in Roblox Studio](/images/plugin-buttons.png) ![Location of Rojo's plugin buttons in Roblox Studio](../images/plugin-buttons.png)
{: align="center" } {: align="center" }
Press **Test Connection** to verify that the plugin can communicate with the dev server. Watch the Output panel for the results. Press **Test Connection** to verify that the plugin can communicate with the dev server. Watch the Output panel for the results.
@@ -64,7 +64,7 @@ Press **Test Connection** to verify that the plugin can communicate with the dev
!!! info !!! info
If you see an error message, return to the previous steps and make sure that the Rojo dev server is running. If you see an error message, return to the previous steps and make sure that the Rojo dev server is running.
![Rojo error in Roblox Studio Output](/images/connection-error.png) ![Rojo error in Roblox Studio Output](../images/connection-error.png)
{: align="center" } {: align="center" }
After your connection was successful, press **Sync In** to move code from the filesystem into Studio, or use **Toggle Polling** to have Rojo automatically sync in changes as they happen. After your connection was successful, press **Sync In** to move code from the filesystem into Studio, or use **Toggle Polling** to have Rojo automatically sync in changes as they happen.

View File

@@ -20,4 +20,7 @@ To install the plugin, either:
* Install the plugin from the [Roblox plugin page](https://www.roblox.com/library/1211549683/Rojo). * Install the plugin from the [Roblox plugin page](https://www.roblox.com/library/1211549683/Rojo).
* This gives you less control over what version you install -- you will always have the latest version. * This gives you less control over what version you install -- you will always have the latest version.
* Or, download the latest release from [the GitHub releases section](https://github.com/LPGhatguy/rojo/releases) and install it into your Roblox plugins folder * Or, download the latest release from [the GitHub releases section](https://github.com/LPGhatguy/rojo/releases) and install it into your Roblox plugins folder
* You can open this folder by clicking the "Plugins Folder" button from the Plugins toolbar in Roblox Studio * You can open this folder by clicking the "Plugins Folder" button from the Plugins toolbar in Roblox Studio
## Visual Studio Code Extension
If you use Visual Studio Code on Windows, you can install [Evaera's unofficial Rojo extension](https://marketplace.visualstudio.com/items?itemName=evaera.vscode-rojo), which will install both halves of Rojo for you. It even has a nifty UI to add partitions and start/stop the Rojo server!

View File

@@ -1,6 +1,16 @@
# Home This is the documentation home for **Rojo 0.4.x**.
This is the documentation home for Rojo.
**Rojo** is a flexible multi-tool designed for creating robust Roblox projects. !!! warning
Rojo 0.4.x has reached end-of-life as of the release of Rojo 0.5.0 on August 27, 2019.
This documentation is a work in progress, and is incomplete. Upgrading to Rojo 0.5.0 is strongly recommended to get new features and continue getting support.
Available versions of these docs:
* [Latest version from `master` branch](https://rojo.space/docs/latest)
* [0.5.x](https://rojo.space/docs/0.5.x)
* [0.4.x](https://rojo.space/docs/0.4.x)
**Rojo** is a tool designed to enable Roblox developers to use professional-grade software engineering tools.
This documentation is a continual work in progress. If you find any issues, please file an issue on [Rojo's issue tracker](https://github.com/rojo-rbx/rojo/issues)!

View File

@@ -5,7 +5,7 @@ This page aims to describe how Rojo turns files on the filesystem into Roblox ob
Any directory on the filesystem will turn into a `Folder` instance in Roblox, unless that folder matches the name of a service or other existing instance. In those cases, that instance will be preserved. Any directory on the filesystem will turn into a `Folder` instance in Roblox, unless that folder matches the name of a service or other existing instance. In those cases, that instance will be preserved.
## Scripts ## Scripts
Rojo can represent `ModuleScript`, `Script`, and `LocalScript` objects. The default script type is `ModuleScript`, since most scripts in well-structued Roblox projects will be modules. Rojo can represent `ModuleScript`, `Script`, and `LocalScript` objects. The default script type is `ModuleScript`, since most scripts in well-structured Roblox projects will be modules.
| File Name | Instance Type | | File Name | Instance Type |
| -------------- | -------------- | | -------------- | -------------- |
@@ -13,7 +13,7 @@ Rojo can represent `ModuleScript`, `Script`, and `LocalScript` objects. The defa
| `*.client.lua` | `LocalScript` | | `*.client.lua` | `LocalScript` |
| `*.lua` | `ModuleScript` | | `*.lua` | `ModuleScript` |
If a directory contains a file named `init.server.lua`, `init.client.lua`, or `init.lua`, that folder will be transformed into a `*Script` instance with the conents of the `init` file. This can be used to create scripts inside of scripts. If a directory contains a file named `init.server.lua`, `init.client.lua`, or `init.lua`, that folder will be transformed into a `*Script` instance with the contents of the `init` file. This can be used to create scripts inside of scripts.
For example, this file tree: For example, this file tree:
@@ -23,16 +23,24 @@ For example, this file tree:
Will turn into these instances in Roblox: Will turn into these instances in Roblox:
![Example of Roblox instances](/images/sync-example.png) ![Example of Roblox instances](images/sync-example.png)
## Models ## Models
Rojo supports a JSON model format for representing simple models. It's designed for instance types like `BindableEvent` or `*Value` objects, and is not suitable for larger models. Rojo supports a JSON model format for representing simple models. It's designed for instance types like `BindableEvent` or `Value` objects, and is not suitable for larger models.
Rojo JSON models are stored in `.model.json` files.
Starting in Rojo version **0.4.10**, model files named `init.model.json` that are located in folders will replace that folder, much like Rojo's `init.lua` support. This can be useful to version instances like `Tool` that tend to contain several instances as well as one or more scripts.
!!! info !!! info
In the future, Rojo will support `.rbxmx` models. See [issue #7](https://github.com/LPGhatguy/rojo/issues/7) for more details and updates on this feature. In the future, Rojo will support `.rbxmx` models. See [issue #7](https://github.com/LPGhatguy/rojo/issues/7) for more details and updates on this feature.
JSON model files are strict, with every property being required. They look like this: !!! warning
Prior to Rojo version **0.4.9**, the `Properties` and `Children` properties are required on all instances in JSON models!
JSON model files are fairly strict; any syntax errors will cause the model to fail to sync! They look like this:
`hello.model.json`
```json ```json
{ {
"Name": "hello", "Name": "hello",
@@ -40,14 +48,11 @@ JSON model files are strict, with every property being required. They look like
"Children": [ "Children": [
{ {
"Name": "Some Part", "Name": "Some Part",
"ClassName": "Part", "ClassName": "Part"
"Children": [],
"Properties": {}
}, },
{ {
"Name": "Some StringValue", "Name": "Some StringValue",
"ClassName": "StringValue", "ClassName": "StringValue",
"Children": [],
"Properties": { "Properties": {
"Value": { "Value": {
"Type": "String", "Type": "String",
@@ -55,7 +60,6 @@ JSON model files are strict, with every property being required. They look like
} }
} }
} }
], ]
"Properties": {}
} }
``` ```

View File

@@ -1,5 +1,5 @@
site_name: Rojo Documentation site_name: Rojo Documentation
site_url: https://lpghatguy.github.io/rojo/ site_url: https://lpghatguy.github.io/rojo/0.4.x
repo_name: LPGhatguy/rojo repo_name: LPGhatguy/rojo
repo_url: https://github.com/LPGhatguy/rojo repo_url: https://github.com/LPGhatguy/rojo
@@ -9,7 +9,7 @@ theme:
primary: 'Red' primary: 'Red'
accent: 'Red' accent: 'Red'
pages: nav:
- Home: index.md - Home: index.md
- Why Rojo?: why-rojo.md - Why Rojo?: why-rojo.md
- Getting Started: - Getting Started:

View File

@@ -39,10 +39,6 @@ stds.testez = {
ignore = { ignore = {
"212", -- unused arguments "212", -- unused arguments
"421", -- shadowing local variable
"422", -- shadowing argument
"431", -- shadowing upvalue
"432", -- shadowing upvalue argument
} }
std = "lua51+roblox" std = "lua51+roblox"

View File

@@ -35,6 +35,9 @@ function Api.connect(http)
setmetatable(context, Api) setmetatable(context, Api)
return context:_start() return context:_start()
:andThen(function()
return context
end)
end end
function Api:_start() function Api:_start()
@@ -60,8 +63,6 @@ function Api:_start()
self.serverId = response.serverId self.serverId = response.serverId
self.currentTime = response.currentTime self.currentTime = response.currentTime
return self
end) end)
end end

View File

@@ -1,7 +1,12 @@
return { return {
pollingRate = 0.2, pollingRate = 0.3,
version = {0, 4, 5}, version = {0, 4, 13},
expectedServerVersionString = "0.4.x", expectedServerVersionString = "0.4.x",
protocolVersion = 1, protocolVersion = 1,
icons = {
syncIn = "rbxassetid://1820320573",
togglePolling = "rbxassetid://1820320064",
testConnection = "rbxassetid://1820320989",
},
dev = false, dev = false,
} }

View File

@@ -45,7 +45,7 @@ local function main()
local toolbar = plugin:CreateToolbar("Rojo Plugin " .. displayedVersion) local toolbar = plugin:CreateToolbar("Rojo Plugin " .. displayedVersion)
toolbar:CreateButton("Test Connection", "Connect to Rojo Server", "") toolbar:CreateButton("Test Connection", "Connect to Rojo Server", Config.icons.testConnection)
.Click:Connect(function() .Click:Connect(function()
checkUpgrade() checkUpgrade()
@@ -55,17 +55,22 @@ local function main()
end) end)
end) end)
toolbar:CreateButton("Sync In", "Sync into Roblox Studio", "") local function syncIn()
.Click:Connect(function() checkUpgrade()
checkUpgrade()
pluginInstance:syncIn() pluginInstance:syncIn()
:catch(function(err) :catch(function(err)
warn(err) warn(err)
end) end)
end) end
toolbar:CreateButton("Toggle Polling", "Poll server for changes", "") local shortDescription = "Sync In"
local longDescription = "Sync into Roblox Studio"
toolbar:CreateButton(shortDescription, longDescription, Config.icons.syncIn).Click:Connect(syncIn)
plugin:CreatePluginAction("RojoSyncIn", shortDescription, longDescription).Triggered:Connect(syncIn)
toolbar:CreateButton("Toggle Polling", "Poll server for changes", Config.icons.togglePolling)
.Click:Connect(function() .Click:Connect(function()
checkUpgrade() checkUpgrade()

View File

@@ -1,9 +1,15 @@
local CoreGui = game:GetService("CoreGui")
local Promise = require(script.Parent.Parent.modules.Promise) local Promise = require(script.Parent.Parent.modules.Promise)
local Config = require(script.Parent.Config) local Config = require(script.Parent.Config)
local Http = require(script.Parent.Http) local Http = require(script.Parent.Http)
local Api = require(script.Parent.Api) local Api = require(script.Parent.Api)
local Reconciler = require(script.Parent.Reconciler) local Reconciler = require(script.Parent.Reconciler)
local Version = require(script.Parent.Version)
local MESSAGE_SERVER_CHANGED = "Rojo: The server has changed since the last request, reloading plugin..."
local MESSAGE_PLUGIN_CHANGED = "Rojo: Another instance of Rojo came online, unloading..."
local function collectMatch(source, pattern) local function collectMatch(source, pattern)
local result = {} local result = {}
@@ -35,9 +41,23 @@ function Plugin.new()
setmetatable(self, Plugin) setmetatable(self, Plugin)
do do
local uiName = ("Rojo %s UI"):format(Version.display(Config.version))
if Config.dev then
uiName = "Rojo Dev UI"
end
-- If there's an existing Rojo UI, like from a Roblox plugin upgrade
-- that wasn't Rojo, make sure we clean it up.
local existingUi = CoreGui:FindFirstChild(uiName)
if existingUi ~= nil then
existingUi:Destroy()
end
local screenGui = Instance.new("ScreenGui") local screenGui = Instance.new("ScreenGui")
screenGui.Name = "Rojo UI" screenGui.Name = uiName
screenGui.Parent = game.CoreGui screenGui.Parent = CoreGui
screenGui.DisplayOrder = -1 screenGui.DisplayOrder = -1
screenGui.Enabled = false screenGui.Enabled = false
@@ -54,6 +74,19 @@ function Plugin.new()
label.Parent = screenGui label.Parent = screenGui
self._label = screenGui self._label = screenGui
-- If our UI was destroyed, we assume it was from another instance of
-- the Rojo plugin coming online.
--
-- Roblox doesn't notify plugins when they get unloaded, so this is the
-- best trigger we have right now unless we create a dedicated event
-- object.
screenGui.AncestryChanged:Connect(function(_, parent)
if parent == nil then
warn(MESSAGE_PLUGIN_CHANGED)
self:restart()
end
end)
end end
return self return self
@@ -64,30 +97,35 @@ end
restarted. restarted.
]] ]]
function Plugin:restart() function Plugin:restart()
warn("Rojo: The server has changed since the last request, reloading plugin...") self:stopPolling()
self._reconciler:destruct()
self._reconciler = Reconciler.new()
self._reconciler:clear()
self._api = nil self._api = nil
self._polling = false self._polling = false
self._syncInProgress = false self._syncInProgress = false
end end
function Plugin:api() function Plugin:getApi()
if not self._api then if self._api == nil then
self._api = Api.connect(self._http) return Api.connect(self._http)
:catch(function(err) :andThen(function(api)
self._api = nil self._api = api
return api
end, function(err)
return Promise.reject(err) return Promise.reject(err)
end) end)
end end
return self._api return Promise.resolve(self._api)
end end
function Plugin:connect() function Plugin:connect()
print("Rojo: Testing connection...") print("Rojo: Testing connection...")
return self:api() return self:getApi()
:andThen(function(api) :andThen(function(api)
local ok, info = api:getInfo():await() local ok, info = api:getInfo():await()
@@ -101,6 +139,7 @@ function Plugin:connect()
end) end)
:catch(function(err) :catch(function(err)
if err == Api.Error.ServerIdMismatch then if err == Api.Error.ServerIdMismatch then
warn(MESSAGE_SERVER_CHANGED)
self:restart() self:restart()
return self:connect() return self:connect()
else else
@@ -122,7 +161,7 @@ function Plugin:stopPolling()
return Promise.resolve(false) return Promise.resolve(false)
end end
print("Rojo Stopped polling server for changes.") print("Rojo: Stopped polling server for changes.")
self._polling = false self._polling = false
self._label.Enabled = false self._label.Enabled = false
@@ -130,38 +169,34 @@ function Plugin:stopPolling()
return Promise.resolve(true) return Promise.resolve(true)
end end
function Plugin:_pull(api, project, routes) function Plugin:_pull(api, project, fileRoutes)
return api:read(routes) return api:read(fileRoutes)
:andThen(function(items) :andThen(function(items)
for index = 1, #routes do for index = 1, #fileRoutes do
local itemRoute = routes[index] local fileRoute = fileRoutes[index]
local partitionName = itemRoute[1] local partitionName = fileRoute[1]
local partition = project.partitions[partitionName] local partition = project.partitions[partitionName]
local item = items[index] local item = items[index]
local partitionRoute = collectMatch(partition.target, "[^.]+") local partitionTargetRbxRoute = collectMatch(partition.target, "[^.]+")
-- If the item route's length was 1, we need to rename the instance to -- If the item route's length was 1, we need to rename the instance to
-- line up with the partition's root object name. -- line up with the partition's root object name.
-- if item ~= nil and #fileRoute == 1 then
-- This is a HACK! local objectName = partition.target:match("[^.]+$")
if #itemRoute == 1 then item.Name = objectName
if item then
local objectName = partition.target:match("[^.]+$")
item.Name = objectName
end
end end
local fullRoute = {} local itemRbxRoute = {}
for _, piece in ipairs(partitionRoute) do for _, piece in ipairs(partitionTargetRbxRoute) do
table.insert(fullRoute, piece) table.insert(itemRbxRoute, piece)
end end
for i = 2, #itemRoute do for i = 2, #fileRoute do
table.insert(fullRoute, itemRoute[i]) table.insert(itemRbxRoute, fileRoute[i])
end end
self._reconciler:reconcileRoute(fullRoute, item, itemRoute) self._reconciler:reconcileRoute(itemRbxRoute, item, fileRoute)
end end
end) end)
end end
@@ -171,25 +206,25 @@ function Plugin:startPolling()
return return
end end
print("Rojo: Polling server for changes...") print("Rojo: Starting to poll server for changes...")
self._polling = true self._polling = true
self._label.Enabled = true self._label.Enabled = true
return self:api() return self:getApi()
:andThen(function(api) :andThen(function(api)
local syncOk, result = self:syncIn():await()
if not syncOk then
return Promise.reject(result)
end
local infoOk, info = api:getInfo():await() local infoOk, info = api:getInfo():await()
if not infoOk then if not infoOk then
return Promise.reject(info) return Promise.reject(info)
end end
local syncOk, result = self:syncIn():await()
if not syncOk then
return Promise.reject(result)
end
while self._polling do while self._polling do
local changesOk, changes = api:getChanges():await() local changesOk, changes = api:getChanges():await()
@@ -215,12 +250,12 @@ function Plugin:startPolling()
end end
end) end)
:catch(function(err) :catch(function(err)
self:stopPolling()
if err == Api.Error.ServerIdMismatch then if err == Api.Error.ServerIdMismatch then
warn(MESSAGE_SERVER_CHANGED)
self:restart() self:restart()
return self:startPolling() return self:startPolling()
else else
self:stopPolling()
return Promise.reject(err) return Promise.reject(err)
end end
end) end)
@@ -236,7 +271,7 @@ function Plugin:syncIn()
self._syncInProgress = true self._syncInProgress = true
print("Rojo: Syncing from server...") print("Rojo: Syncing from server...")
return self:api() return self:getApi()
:andThen(function(api) :andThen(function(api)
local ok, info = api:getInfo():await() local ok, info = api:getInfo():await()
@@ -244,21 +279,27 @@ function Plugin:syncIn()
return Promise.reject(info) return Promise.reject(info)
end end
local routes = {} local fileRoutes = {}
for name in pairs(info.project.partitions) do for name in pairs(info.project.partitions) do
table.insert(routes, {name}) table.insert(fileRoutes, {name})
end end
self:_pull(api, info.project, routes) local pullSuccess, pullResult = self:_pull(api, info.project, fileRoutes):await()
self._syncInProgress = false self._syncInProgress = false
if not pullSuccess then
return Promise.reject(pullResult)
end
print("Rojo: Sync successful!") print("Rojo: Sync successful!")
end) end)
:catch(function(err) :catch(function(err)
self._syncInProgress = false self._syncInProgress = false
if err == Api.Error.ServerIdMismatch then if err == Api.Error.ServerIdMismatch then
warn(MESSAGE_SERVER_CHANGED)
self:restart() self:restart()
return self:syncIn() return self:syncIn()
else else

View File

@@ -1,24 +1,48 @@
local RouteMap = require(script.Parent.RouteMap) local RouteMap = require(script.Parent.RouteMap)
local function classEqual(rbx, className) local function classEqual(a, b)
if className == "*" then assert(typeof(a) == "string")
assert(typeof(b) == "string")
if a == "*" or b == "*" then
return true return true
end end
return rbx.ClassName == className return a == b
end end
local function reparent(rbx, parent) local function applyProperties(target, properties)
if rbx then assert(typeof(target) == "Instance")
-- It's possible that 'rbx' is a service or some other object that we assert(typeof(properties) == "table")
-- can't change the parent of. That's the only reason why Parent would
-- fail except for rbx being previously destroyed! for key, property in pairs(properties) do
pcall(function() -- TODO: Transform property value based on property.Type
rbx.Parent = parent -- Right now, we assume that 'value' is primitive!
end) target[key] = property.Value
end end
end end
--[[
Attempt to parent `rbx` to `parent`, doing nothing if:
* parent is already `parent`
* Changing parent threw an error
]]
local function reparent(rbx, parent)
assert(typeof(rbx) == "Instance")
assert(typeof(parent) == "Instance")
if rbx.Parent == parent then
return
end
-- Setting `Parent` can fail if:
-- * The object has been destroyed
-- * The object is a service and cannot be reparented
pcall(function()
rbx.Parent = parent
end)
end
--[[ --[[
Attempts to match up Roblox instances and object specifiers for Attempts to match up Roblox instances and object specifiers for
reconciliation. reconciliation.
@@ -38,7 +62,7 @@ local function findNextChildPair(primaryChildren, secondaryChildren, visited)
visited[primaryChild] = true visited[primaryChild] = true
for _, secondaryChild in ipairs(secondaryChildren) do for _, secondaryChild in ipairs(secondaryChildren) do
if primaryChild.ClassName == secondaryChild.ClassName and primaryChild.Name == secondaryChild.Name then if classEqual(primaryChild.ClassName, secondaryChild.ClassName) and primaryChild.Name == secondaryChild.Name then
visited[secondaryChild] = true visited[secondaryChild] = true
return primaryChild, secondaryChild return primaryChild, secondaryChild
@@ -77,22 +101,30 @@ function Reconciler:_reconcileChildren(rbx, item)
while true do while true do
local itemChild, rbxChild = findNextChildPair(item.Children, rbxChildren, visited) local itemChild, rbxChild = findNextChildPair(item.Children, rbxChildren, visited)
if not itemChild then if itemChild == nil then
break break
end end
reparent(self:reconcile(rbxChild, itemChild), rbx) local newRbxChild = self:reconcile(rbxChild, itemChild)
if newRbxChild ~= nil then
newRbxChild.Parent = rbx
end
end end
-- Reconcile any children that were deleted -- Reconcile any children that were deleted
while true do while true do
local rbxChild, itemChild = findNextChildPair(rbxChildren, item.Children, visited) local rbxChild, itemChild = findNextChildPair(rbxChildren, item.Children, visited)
if not rbxChild then if rbxChild == nil then
break break
end end
reparent(self:reconcile(rbxChild, itemChild), rbx) local newRbxChild = self:reconcile(rbxChild, itemChild)
if newRbxChild ~= nil then
newRbxChild.Parent = rbx
end
end end
end end
@@ -110,14 +142,13 @@ function Reconciler:_reify(item)
local rbx = Instance.new(className) local rbx = Instance.new(className)
rbx.Name = item.Name rbx.Name = item.Name
for key, property in pairs(item.Properties) do applyProperties(rbx, item.Properties)
-- TODO: Check for compound types, like Vector3!
rbx[key] = property.Value for _, child in ipairs(item.Children) do
reparent(self:_reify(child), rbx)
end end
self:_reconcileChildren(rbx, item) if item.Route ~= nil then
if item.Route then
self._routeMap:insert(item.Route, rbx) self._routeMap:insert(item.Route, rbx)
end end
@@ -125,10 +156,10 @@ function Reconciler:_reify(item)
end end
--[[ --[[
Clears any state that the Reconciler has, effectively restarting it. Clears any state that the Reconciler has, stopping it completely.
]] ]]
function Reconciler:clear() function Reconciler:destruct()
self._routeMap:clear() self._routeMap:destruct()
end end
--[[ --[[
@@ -137,8 +168,11 @@ end
]] ]]
function Reconciler:reconcile(rbx, item) function Reconciler:reconcile(rbx, item)
-- Item was deleted -- Item was deleted
if not item then if item == nil then
if rbx then if rbx ~= nil then
-- TODO: If this is a partition root, should we leave it alone?
self._routeMap:removeByRbx(rbx)
rbx:Destroy() rbx:Destroy()
end end
@@ -146,52 +180,52 @@ function Reconciler:reconcile(rbx, item)
end end
-- Item was created! -- Item was created!
if not rbx then if rbx == nil then
return self:_reify(item) return self:_reify(item)
end end
-- Item changed type! -- Item changed type!
if not classEqual(rbx, item.ClassName) then if not classEqual(rbx.ClassName, item.ClassName) then
self._routeMap:removeByRbx(rbx)
rbx:Destroy() rbx:Destroy()
rbx = self:_reify(item) return self:_reify(item)
end end
-- Apply all properties, Roblox will de-duplicate changes -- It's possible that the instance we're associating with this item hasn't
for key, property in pairs(item.Properties) do -- been inserted into the RouteMap yet.
-- TODO: Transform property value based on property.Type if item.Route ~= nil then
-- Right now, we assume that 'value' is primitive!
rbx[key] = property.Value
end
-- Use a dumb algorithm for reconciling children
self:_reconcileChildren(rbx, item)
if item.Route then
self._routeMap:insert(item.Route, rbx) self._routeMap:insert(item.Route, rbx)
end end
applyProperties(rbx, item.Properties)
self:_reconcileChildren(rbx, item)
return rbx return rbx
end end
function Reconciler:reconcileRoute(route, item, itemRoute) function Reconciler:reconcileRoute(rbxRoute, item, fileRoute)
local parent local parent
local rbx = game local rbx = game
for i = 1, #route do for i = 1, #rbxRoute do
local piece = route[i] local piece = rbxRoute[i]
local child = rbx:FindFirstChild(piece) local child = rbx:FindFirstChild(piece)
-- We should get services instead of making folders here. -- We should get services instead of making folders here.
if rbx == game and not child then if rbx == game and child == nil then
local _ local success
_, child = pcall(game.GetService, game, piece) success, child = pcall(game.GetService, game, piece)
-- That isn't a valid service!
if not success then
child = nil
end
end end
-- We don't want to create a folder if we're reaching our target item! -- We don't want to create a folder if we're reaching our target item!
if not child and i ~= #route then if child == nil and i ~= #rbxRoute then
child = Instance.new("Folder") child = Instance.new("Folder")
child.Parent = rbx child.Parent = rbx
child.Name = piece child.Name = piece
@@ -202,13 +236,15 @@ function Reconciler:reconcileRoute(route, item, itemRoute)
end end
-- Let's check the route map! -- Let's check the route map!
if not rbx then if rbx == nil then
rbx = self._routeMap:get(itemRoute) rbx = self._routeMap:get(fileRoute)
end end
rbx = self:reconcile(rbx, item) rbx = self:reconcile(rbx, item)
reparent(rbx, parent) if rbx ~= nil then
reparent(rbx, parent)
end
end end
return Reconciler return Reconciler

View File

@@ -25,6 +25,10 @@ end
function RouteMap:insert(route, rbx) function RouteMap:insert(route, rbx)
local hashed = hashRoute(route) local hashed = hashRoute(route)
-- Make sure that each route and instance are only present in RouteMap once.
self:removeByRoute(route)
self:removeByRbx(rbx)
self._map[hashed] = rbx self._map[hashed] = rbx
self._reverseMap[rbx] = hashed self._reverseMap[rbx] = hashed
self._connectionsByRbx[rbx] = rbx.AncestryChanged:Connect(function(_, parent) self._connectionsByRbx[rbx] = rbx.AncestryChanged:Connect(function(_, parent)
@@ -42,24 +46,36 @@ function RouteMap:removeByRoute(route)
local hashedRoute = hashRoute(route) local hashedRoute = hashRoute(route)
local rbx = self._map[hashedRoute] local rbx = self._map[hashedRoute]
if rbx then if rbx ~= nil then
self._map[hashedRoute] = nil self:_removeInternal(rbx, hashedRoute)
self._reverseMap[rbx] = nil
self._connectionsByRbx[rbx] = nil
end end
end end
function RouteMap:removeByRbx(rbx) function RouteMap:removeByRbx(rbx)
local hashedRoute = self._reverseMap[rbx] local hashedRoute = self._reverseMap[rbx]
if hashedRoute then if hashedRoute ~= nil then
self._map[hashedRoute] = nil self:_removeInternal(rbx, hashedRoute)
self._reverseMap[rbx] = nil
self._connectionsByRbx[rbx] = nil
end end
end end
function RouteMap:removeRbxDescendants(parentRbx) --[[
Correcly removes the given Roblox Instance/Route pair from the RouteMap.
]]
function RouteMap:_removeInternal(rbx, hashedRoute)
self._map[hashedRoute] = nil
self._reverseMap[rbx] = nil
self._connectionsByRbx[rbx]:Disconnect()
self._connectionsByRbx[rbx] = nil
self:_removeRbxDescendants(rbx)
end
--[[
Ensure that there are no descendants of the given Roblox Instance still
present in the map, guaranteeing that it has been cleaned out.
]]
function RouteMap:_removeRbxDescendants(parentRbx)
for rbx in pairs(self._reverseMap) do for rbx in pairs(self._reverseMap) do
if rbx:IsDescendantOf(parentRbx) then if rbx:IsDescendantOf(parentRbx) then
self:removeByRbx(rbx) self:removeByRbx(rbx)
@@ -67,7 +83,11 @@ function RouteMap:removeRbxDescendants(parentRbx)
end end
end end
function RouteMap:clear() --[[
Remove all items from the map and disconnect all connections, cleaning up
the RouteMap.
]]
function RouteMap:destruct()
self._map = {} self._map = {}
self._reverseMap = {} self._reverseMap = {}

2
server/Cargo.lock generated
View File

@@ -636,7 +636,7 @@ dependencies = [
[[package]] [[package]]
name = "rojo" name = "rojo"
version = "0.4.5" version = "0.4.13"
dependencies = [ dependencies = [
"clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rojo" name = "rojo"
version = "0.4.5" version = "0.4.13"
authors = ["Lucien Greathouse <me@lpghatguy.com>"] authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "A tool to create robust Roblox projects" description = "A tool to create robust Roblox projects"
license = "MIT" license = "MIT"

View File

@@ -44,6 +44,13 @@ pub fn serve(project_path: &PathBuf, verbose: bool, port: Option<u64>) {
}, },
}; };
if project.partitions.len() == 0 {
println!("");
println!("This project has no partitions and will not do anything when served!");
println!("This is usually a mistake -- edit rojo.json!");
println!("");
}
lazy_static! { lazy_static! {
static ref PLUGIN_CHAIN: PluginChain = PluginChain::new(vec![ static ref PLUGIN_CHAIN: PluginChain = PluginChain::new(vec![
Box::new(ScriptPlugin::new()), Box::new(ScriptPlugin::new()),

View File

@@ -9,13 +9,6 @@ pub enum TransformFileResult {
// TODO: Error case // TODO: Error case
} }
pub enum RbxChangeResult {
Write(Option<VfsItem>),
Pass,
// TODO: Error case
}
pub enum FileChangeResult { pub enum FileChangeResult {
MarkChanged(Option<Vec<Route>>), MarkChanged(Option<Vec<Route>>),
Pass, Pass,
@@ -26,10 +19,6 @@ pub trait Plugin {
/// into a Roblox instance. /// into a Roblox instance.
fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult; fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult;
/// Invoked when a Roblox Instance change is reported by the Roblox Studio
/// plugin and needs to be turned into a file to save.
fn handle_rbx_change(&self, route: &Route, rbx_item: &RbxInstance) -> RbxChangeResult;
/// Invoked when a file changes on the filesystem. The result defines what /// Invoked when a file changes on the filesystem. The result defines what
/// routes are marked as needing to be refreshed. /// routes are marked as needing to be refreshed.
fn handle_file_change(&self, route: &Route) -> FileChangeResult; fn handle_file_change(&self, route: &Route) -> FileChangeResult;
@@ -58,17 +47,6 @@ impl PluginChain {
None None
} }
pub fn handle_rbx_change(&self, route: &Route, rbx_item: &RbxInstance) -> Option<VfsItem> {
for plugin in &self.plugins {
match plugin.handle_rbx_change(route, rbx_item) {
RbxChangeResult::Write(vfs_item) => return vfs_item,
RbxChangeResult::Pass => {},
}
}
None
}
pub fn handle_file_change(&self, route: &Route) -> Option<Vec<Route>> { pub fn handle_file_change(&self, route: &Route) -> Option<Vec<Route>> {
for plugin in &self.plugins { for plugin in &self.plugins {
match plugin.handle_file_change(route) { match plugin.handle_file_change(route) {

View File

@@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use core::Route; use core::Route;
use plugin::{Plugin, PluginChain, TransformFileResult, RbxChangeResult, FileChangeResult}; use plugin::{Plugin, PluginChain, TransformFileResult, FileChangeResult};
use rbx::{RbxInstance, RbxValue}; use rbx::{RbxInstance, RbxValue};
use vfs::VfsItem; use vfs::VfsItem;
@@ -60,8 +60,4 @@ impl Plugin for DefaultPlugin {
fn handle_file_change(&self, route: &Route) -> FileChangeResult { fn handle_file_change(&self, route: &Route) -> FileChangeResult {
FileChangeResult::MarkChanged(Some(vec![route.clone()])) FileChangeResult::MarkChanged(Some(vec![route.clone()]))
} }
fn handle_rbx_change(&self, _route: &Route, _rbx_item: &RbxInstance) -> RbxChangeResult {
RbxChangeResult::Pass
}
} }

View File

@@ -2,7 +2,7 @@ use regex::Regex;
use serde_json; use serde_json;
use core::Route; use core::Route;
use plugin::{Plugin, PluginChain, TransformFileResult, RbxChangeResult, FileChangeResult}; use plugin::{Plugin, PluginChain, TransformFileResult, FileChangeResult};
use rbx::RbxInstance; use rbx::RbxInstance;
use vfs::VfsItem; use vfs::VfsItem;
@@ -10,6 +10,8 @@ lazy_static! {
static ref JSON_MODEL_PATTERN: Regex = Regex::new(r"^(.*?)\.model\.json$").unwrap(); static ref JSON_MODEL_PATTERN: Regex = Regex::new(r"^(.*?)\.model\.json$").unwrap();
} }
static JSON_MODEL_INIT: &'static str = "init.model.json";
pub struct JsonModelPlugin; pub struct JsonModelPlugin;
impl JsonModelPlugin { impl JsonModelPlugin {
@@ -19,7 +21,7 @@ impl JsonModelPlugin {
} }
impl Plugin for JsonModelPlugin { impl Plugin for JsonModelPlugin {
fn transform_file(&self, _plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult { fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult {
match vfs_item { match vfs_item {
&VfsItem::File { ref contents, .. } => { &VfsItem::File { ref contents, .. } => {
let rbx_name = match JSON_MODEL_PATTERN.captures(vfs_item.name()) { let rbx_name = match JSON_MODEL_PATTERN.captures(vfs_item.name()) {
@@ -41,15 +43,57 @@ impl Plugin for JsonModelPlugin {
TransformFileResult::Value(Some(rbx_item)) TransformFileResult::Value(Some(rbx_item))
}, },
&VfsItem::Dir { .. } => TransformFileResult::Pass, &VfsItem::Dir { ref children, .. } => {
let init_item = match children.get(JSON_MODEL_INIT) {
Some(v) => v,
None => return TransformFileResult::Pass,
};
let mut rbx_item = match self.transform_file(plugins, init_item) {
TransformFileResult::Value(Some(item)) => item,
TransformFileResult::Value(None) | TransformFileResult::Pass => {
eprintln!("Inconsistency detected in JsonModelPlugin!");
return TransformFileResult::Pass;
},
};
rbx_item.name.clear();
rbx_item.name.push_str(vfs_item.name());
rbx_item.route = Some(vfs_item.route().to_vec());
for (child_name, child_item) in children {
if child_name == init_item.name() {
continue;
}
match plugins.transform_file(child_item) {
Some(child_rbx_item) => {
rbx_item.children.push(child_rbx_item);
},
_ => {},
}
}
TransformFileResult::Value(Some(rbx_item))
},
} }
} }
fn handle_file_change(&self, _route: &Route) -> FileChangeResult { fn handle_file_change(&self, route: &Route) -> FileChangeResult {
FileChangeResult::Pass let leaf = match route.last() {
} Some(v) => v,
None => return FileChangeResult::Pass,
};
fn handle_rbx_change(&self, _route: &Route, _rbx_item: &RbxInstance) -> RbxChangeResult { let is_init = leaf == JSON_MODEL_INIT;
RbxChangeResult::Pass
if is_init {
let mut changed = route.clone();
changed.pop();
FileChangeResult::MarkChanged(Some(vec![changed]))
} else {
FileChangeResult::Pass
}
} }
} }

View File

@@ -3,7 +3,7 @@ use std::collections::HashMap;
use regex::Regex; use regex::Regex;
use core::Route; use core::Route;
use plugin::{Plugin, PluginChain, TransformFileResult, RbxChangeResult, FileChangeResult}; use plugin::{Plugin, PluginChain, TransformFileResult, FileChangeResult};
use rbx::{RbxInstance, RbxValue}; use rbx::{RbxInstance, RbxValue};
use vfs::VfsItem; use vfs::VfsItem;
@@ -79,6 +79,7 @@ impl Plugin for ScriptPlugin {
rbx_item.name.clear(); rbx_item.name.clear();
rbx_item.name.push_str(vfs_item.name()); rbx_item.name.push_str(vfs_item.name());
rbx_item.route = Some(vfs_item.route().to_vec());
for (child_name, child_item) in children { for (child_name, child_item) in children {
if child_name == init_item.name() { if child_name == init_item.name() {
@@ -117,8 +118,4 @@ impl Plugin for ScriptPlugin {
FileChangeResult::Pass FileChangeResult::Pass
} }
} }
fn handle_rbx_change(&self, _route: &Route, _rbx_item: &RbxInstance) -> RbxChangeResult {
RbxChangeResult::Pass
}
} }

View File

@@ -6,7 +6,11 @@ use std::collections::HashMap;
pub struct RbxInstance { pub struct RbxInstance {
pub name: String, pub name: String,
pub class_name: String, pub class_name: String,
#[serde(default = "Vec::new")]
pub children: Vec<RbxInstance>, pub children: Vec<RbxInstance>,
#[serde(default = "HashMap::new")]
pub properties: HashMap<String, RbxValue>, pub properties: HashMap<String, RbxValue>,
/// The route that this instance was generated from, if there was one. /// The route that this instance was generated from, if there was one.

View File

@@ -83,7 +83,8 @@ impl VfsWatcher {
match watcher.watch(&root_path, RecursiveMode::Recursive) { match watcher.watch(&root_path, RecursiveMode::Recursive) {
Ok(_) => (), Ok(_) => (),
Err(_) => { Err(_) => {
panic!("Unable to watch partition {}, with path {}! Make sure that it's a file or directory.", partition_name, root_path.display()); eprintln!("WARNING: Unable to watch partition {}, with path {}\nMake sure that it's a file or directory.", partition_name, root_path.display());
continue;
}, },
} }

View File

@@ -9,6 +9,10 @@
"extra": { "extra": {
"path": "extra-script.lua", "path": "extra-script.lua",
"target": "ReplicatedStorage.ExtraScript" "target": "ReplicatedStorage.ExtraScript"
},
"does-not-exist": {
"path": "hahah",
"target": "ReplicatedStorage.Never"
} }
} }
} }

View File

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

View File

@@ -0,0 +1,4 @@
{
"Name": "SomeTool",
"ClassName": "Tool"
}

1
test-project/src/a/b.lua Normal file
View File

@@ -0,0 +1 @@
print("HEY!")

View File

@@ -4,14 +4,11 @@
"Children": [ "Children": [
{ {
"Name": "Some Part", "Name": "Some Part",
"ClassName": "Part", "ClassName": "Part"
"Children": [],
"Properties": {}
}, },
{ {
"Name": "Some StringValue", "Name": "Some StringValue",
"ClassName": "StringValue", "ClassName": "StringValue",
"Children": [],
"Properties": { "Properties": {
"Value": { "Value": {
"Type": "String", "Type": "String",
@@ -19,6 +16,5 @@
} }
} }
} }
], ]
"Properties": {}
} }