mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-20 20:55:50 +00:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d147ae7bb6 | ||
|
|
9b6ce57c79 | ||
|
|
ed85d2a327 | ||
|
|
7e5b8cadfc | ||
|
|
c805f4fa2d | ||
|
|
f09d7fb0ff | ||
|
|
4e918bab5e | ||
|
|
8e3f9b3bfd | ||
|
|
300975f49c | ||
|
|
325cf56457 | ||
|
|
71b1320aa8 | ||
|
|
c5abc87dbd | ||
|
|
bd7ab593d5 | ||
|
|
c93da3f6b2 | ||
|
|
8b90e98696 | ||
|
|
bc40ec8a5a | ||
|
|
f19cbccdd5 | ||
|
|
f25ae914e4 | ||
|
|
fb7bfa928a | ||
|
|
100d69262c | ||
|
|
5e01658846 | ||
|
|
ccec93aee8 | ||
|
|
a089d82023 | ||
|
|
82ba583fa0 | ||
|
|
1b82044d7d | ||
|
|
0d49a2e0af | ||
|
|
1343d3a2a9 | ||
|
|
a86001b85c | ||
|
|
d6dd46c467 | ||
|
|
320974074c | ||
|
|
7b824abe52 | ||
|
|
bfd33f4b8d | ||
|
|
d5a21a0513 | ||
|
|
c894b38f06 | ||
|
|
a86347ea32 | ||
|
|
b60bfc7495 | ||
|
|
4b2f27b26d | ||
|
|
f4d7dda8e3 | ||
|
|
0d6e3e66ce | ||
|
|
7e4d451765 | ||
|
|
804bbc93b7 | ||
|
|
e7fe4ac3ec | ||
|
|
40c41b4400 | ||
|
|
0936c7c97d | ||
|
|
9ac537d38f | ||
|
|
fcfd55ff76 | ||
|
|
c2495ed57f | ||
|
|
6ad763fc01 | ||
|
|
c856a3e361 | ||
|
|
aa5f0cc335 | ||
|
|
b067335bbf | ||
|
|
7d24a14004 | ||
|
|
910be640e9 | ||
|
|
3137753afa | ||
|
|
000ff351a5 | ||
|
|
533c8ddaf7 | ||
|
|
f777d1b6c6 | ||
|
|
8b17d3b7d9 | ||
|
|
6fbe1daf8e | ||
|
|
3bd191414b | ||
|
|
fd2cb3495b | ||
|
|
e9d33bdc02 | ||
|
|
c0f4b31ab3 | ||
|
|
78de30dcf2 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
/site
|
/site
|
||||||
|
/generate-docs
|
||||||
|
|||||||
57
CHANGES.md
57
CHANGES.md
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
BIN
assets/rojo-polling-icon.png
Normal file
BIN
assets/rojo-polling-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 375 B |
BIN
assets/rojo-sync-in.png
Normal file
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
BIN
assets/rojo-test-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 430 B |
@@ -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.
|
||||||
|
|
||||||

|

|
||||||
{: 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.
|
||||||
|
|
||||||

|

|
||||||
{: 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.
|
||||||
|
|||||||
@@ -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!
|
||||||
@@ -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)!
|
||||||
@@ -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:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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": {}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
2
server/Cargo.lock
generated
@@ -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)",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1
test-project/src/SomeTool/SomeTool.client.lua
Normal file
1
test-project/src/SomeTool/SomeTool.client.lua
Normal file
@@ -0,0 +1 @@
|
|||||||
|
print("Hello, world, from my tool!")
|
||||||
4
test-project/src/SomeTool/init.model.json
Normal file
4
test-project/src/SomeTool/init.model.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"Name": "SomeTool",
|
||||||
|
"ClassName": "Tool"
|
||||||
|
}
|
||||||
1
test-project/src/a/b.lua
Normal file
1
test-project/src/a/b.lua
Normal file
@@ -0,0 +1 @@
|
|||||||
|
print("HEY!")
|
||||||
@@ -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": {}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user