mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-21 05:06:29 +00:00
Compare commits
87 Commits
v0.5.0-alp
...
v0.5.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
420627d892 | ||
|
|
ce3a409997 | ||
|
|
0f9f1782ae | ||
|
|
d4704a02c5 | ||
|
|
9ca2ed2c93 | ||
|
|
ae12ffdefb | ||
|
|
1e13097126 | ||
|
|
9b8a6b1168 | ||
|
|
8f6dda5cd3 | ||
|
|
91780f236e | ||
|
|
f16474815c | ||
|
|
a8ff6d7e6e | ||
|
|
8395782a2e | ||
|
|
28ea625b01 | ||
|
|
efc569f6ed | ||
|
|
d377e10771 | ||
|
|
fef85877e6 | ||
|
|
19135bfaf4 | ||
|
|
5a147fccc2 | ||
|
|
20976814ba | ||
|
|
27e2612fc9 | ||
|
|
3ea432ef2d | ||
|
|
fe6acbc1e3 | ||
|
|
379b162e64 | ||
|
|
84832955dd | ||
|
|
34b99a51c3 | ||
|
|
fb5245e2af | ||
|
|
ff0a830e0c | ||
|
|
a365f071a4 | ||
|
|
f290e7b5b2 | ||
|
|
83a0ae673c | ||
|
|
7de646c290 | ||
|
|
5d681a72ac | ||
|
|
d725970e6e | ||
|
|
54b82760cd | ||
|
|
77f79fa913 | ||
|
|
6db714a2b1 | ||
|
|
913ac7c9f5 | ||
|
|
eecbfd29e7 | ||
|
|
41025225b2 | ||
|
|
07c7b28c03 | ||
|
|
3faf3d2a56 | ||
|
|
be094d5b7c | ||
|
|
459673bd59 | ||
|
|
2968b70e6b | ||
|
|
b6989a18fc | ||
|
|
4d6a504836 | ||
|
|
6c3737df68 | ||
|
|
9f382ed9bd | ||
|
|
f9e86e58d6 | ||
|
|
469f9c927f | ||
|
|
312724189b | ||
|
|
ec0a1f1ce4 | ||
|
|
ad93631ef8 | ||
|
|
3b6238ff93 | ||
|
|
5b9facee00 | ||
|
|
376f2a554a | ||
|
|
5fd0bd3db9 | ||
|
|
2deb3bbf23 | ||
|
|
01bef0c2b8 | ||
|
|
b65a8ce680 | ||
|
|
5fc4f63238 | ||
|
|
9b0e0c175b | ||
|
|
eb97e925e6 | ||
|
|
16f8975b18 | ||
|
|
5073fce2f7 | ||
|
|
cf5036eec6 | ||
|
|
20be37dd8b | ||
|
|
93349ae2dc | ||
|
|
be81de74cd | ||
|
|
88e739090d | ||
|
|
7f324f1957 | ||
|
|
4f31c9e72f | ||
|
|
c9a663ed39 | ||
|
|
105d8aeb6b | ||
|
|
6ea1211bc5 | ||
|
|
c13291a598 | ||
|
|
aaa78c618c | ||
|
|
2890c677d4 | ||
|
|
51a010de00 | ||
|
|
ca0aabd814 | ||
|
|
91d1ba1910 | ||
|
|
c7c739dc00 | ||
|
|
7a8389bf11 | ||
|
|
5f062b8ea3 | ||
|
|
b9ee14a0f9 | ||
|
|
c3baf73455 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -2,4 +2,8 @@
|
||||
/target
|
||||
/scratch-project
|
||||
**/*.rs.bk
|
||||
/generate-docs.run
|
||||
/server/failed-snapshots/
|
||||
/*.rbxm
|
||||
/*.rbxmx
|
||||
/*.rbxl
|
||||
/*.rbxlx
|
||||
12
.gitmodules
vendored
12
.gitmodules
vendored
@@ -1,12 +1,6 @@
|
||||
[submodule "plugin/modules/roact"]
|
||||
path = plugin/modules/roact
|
||||
url = https://github.com/Roblox/roact.git
|
||||
[submodule "plugin/modules/rodux"]
|
||||
path = plugin/modules/rodux
|
||||
url = https://github.com/Roblox/rodux.git
|
||||
[submodule "plugin/modules/roact-rodux"]
|
||||
path = plugin/modules/roact-rodux
|
||||
url = https://github.com/Roblox/roact-rodux.git
|
||||
[submodule "plugin/modules/testez"]
|
||||
path = plugin/modules/testez
|
||||
url = https://github.com/Roblox/testez.git
|
||||
@@ -16,3 +10,9 @@
|
||||
[submodule "plugin/modules/promise"]
|
||||
path = plugin/modules/promise
|
||||
url = https://github.com/LPGhatguy/roblox-lua-promise.git
|
||||
[submodule "plugin/modules/t"]
|
||||
path = plugin/modules/t
|
||||
url = https://github.com/osyrisrblx/t.git
|
||||
[submodule "plugin/modules/rbx-dom"]
|
||||
path = plugin/modules/rbx-dom
|
||||
url = http://github.com/LPGhatguy/rbx-dom
|
||||
|
||||
51
.travis.yml
51
.travis.yml
@@ -1,36 +1,41 @@
|
||||
matrix:
|
||||
include:
|
||||
- language: python
|
||||
env:
|
||||
- LUA="lua=5.1"
|
||||
# Lua tests are currently disabled because of holes in Lemur that are pretty
|
||||
# tedious to fix. It should be fixed by either adding missing features to
|
||||
# Lemur or by migrating to a CI system based on real Roblox instead.
|
||||
|
||||
before_install:
|
||||
- pip install hererocks
|
||||
- hererocks lua_install -r^ --$LUA
|
||||
- export PATH=$PATH:$PWD/lua_install/bin
|
||||
# - language: python
|
||||
# env:
|
||||
# - LUA="lua=5.1"
|
||||
|
||||
install:
|
||||
- luarocks install luafilesystem
|
||||
- luarocks install busted
|
||||
- luarocks install luacov
|
||||
- luarocks install luacov-coveralls
|
||||
- luarocks install luacheck
|
||||
# before_install:
|
||||
# - pip install hererocks
|
||||
# - hererocks lua_install -r^ --$LUA
|
||||
# - export PATH=$PATH:$PWD/lua_install/bin
|
||||
|
||||
script:
|
||||
- cd plugin
|
||||
- luacheck src
|
||||
- lua -lluacov spec.lua
|
||||
# install:
|
||||
# - luarocks install luafilesystem
|
||||
# - luarocks install busted
|
||||
# - luarocks install luacov
|
||||
# - luarocks install luacov-coveralls
|
||||
# - luarocks install luacheck
|
||||
|
||||
after_success:
|
||||
- cd plugin
|
||||
- luacov-coveralls -e $TRAVIS_BUILD_DIR/lua_install
|
||||
# script:
|
||||
# - cd plugin
|
||||
# - luacheck src
|
||||
# - lua -lluacov spec.lua
|
||||
|
||||
# after_success:
|
||||
# - cd plugin
|
||||
# - luacov-coveralls -e $TRAVIS_BUILD_DIR/lua_install
|
||||
|
||||
- language: rust
|
||||
rust: 1.31.1
|
||||
rust: 1.32.0
|
||||
cache: cargo
|
||||
|
||||
script:
|
||||
- cargo test --verbose
|
||||
- cargo test --verbose --all-features
|
||||
|
||||
- language: rust
|
||||
rust: stable
|
||||
@@ -38,10 +43,12 @@ matrix:
|
||||
|
||||
script:
|
||||
- cargo test --verbose
|
||||
- cargo test --verbose --all-features
|
||||
|
||||
- language: rust
|
||||
rust: beta
|
||||
cache: cargo
|
||||
|
||||
script:
|
||||
- cargo test --verbose
|
||||
- cargo test --verbose
|
||||
- cargo test --verbose --all-features
|
||||
58
CHANGELOG.md
58
CHANGELOG.md
@@ -2,6 +2,64 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.5.0 Alpha 11](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.11) (May 29, 2019)
|
||||
* Added support for implicit property values in JSON model files ([#154](https://github.com/LPGhatguy/rojo/pull/154))
|
||||
* `Content` propertyes can now be specified in projects and model files as regular string literals.
|
||||
* Added support for `BrickColor` properties.
|
||||
* Added support for properties added in client release 384, like `Lighting.Technology` being set to `"ShadowMap"`.
|
||||
* Improved performance when working with XML models and places
|
||||
* Fixed serializing empty `Content` properties as XML
|
||||
* Fixed serializing infinite and NaN floating point properties in XML
|
||||
* Improved compatibility with XML models
|
||||
* Plugin should now be able to live-sync more properties, and ignore ones it can't, like `Lighting.Technology`.
|
||||
|
||||
## 0.5.0 Alpha 10
|
||||
* This release was a dud due to [issue #176](https://github.com/LPGhatguy/rojo/issues/176) and was rolled back.
|
||||
|
||||
## [0.5.0 Alpha 9](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.9) (April 4, 2019)
|
||||
* Changed `rojo build` to use buffered I/O, which can make it up to 2x faster in some cases.
|
||||
* Building [*Road Not Taken*](https://github.com/LPGhatguy/roads) to an `rbxlx` file dropped from 150ms to 70ms on my machine
|
||||
* Fixed `LocalizationTable` instances being made from `csv` files incorrectly interpreting empty rows and columns. ([#149](https://github.com/LPGhatguy/rojo/pull/149))
|
||||
* Fixed CSV files with entries that parse as numbers causing Rojo to panic. ([#152](https://github.com/LPGhatguy/rojo/pull/152))
|
||||
* Improved error messages when malformed CSV files are found in a Rojo project.
|
||||
|
||||
## [0.5.0 Alpha 8](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.8) (March 29, 2019)
|
||||
* Added support for a bunch of new types when dealing with XML model/place files:
|
||||
* `ColorSequence`
|
||||
* `Float64`
|
||||
* `Int64`
|
||||
* `NumberRange`
|
||||
* `NumberSequence`
|
||||
* `PhysicalProperties`
|
||||
* `Ray`
|
||||
* `Rect`
|
||||
* `Ref`
|
||||
* Improved server instance ordering behavior when files are added during a live session ([#135](https://github.com/LPGhatguy/rojo/pull/135))
|
||||
* Fixed error being thrown when trying to unload the Rojo plugin.
|
||||
* Added partial fix for [issue #141](https://github.com/LPGhatguy/rojo/issues/141) for `Lighting.Technology`, which should restore live sync functionality for the default project file.
|
||||
|
||||
## [0.5.0 Alpha 6](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.6) (March 19, 2019)
|
||||
* Fixed `rojo init` giving unexpected results by upgrading to `rbx_dom_weak` 1.1.0
|
||||
* Fixed live server not responding when the Rojo plugin is connected ([#133](https://github.com/LPGhatguy/rojo/issues/133))
|
||||
* Updated default place file:
|
||||
* Improved default properties to be closer to Studio's built-in 'Baseplate' template
|
||||
* Added a baseplate to the project file (Thanks, [@AmaranthineCodices](https://github.com/AmaranthineCodices/)!)
|
||||
* Added more type support to Rojo plugin
|
||||
* Fixed some cases where the Rojo plugin would leave around objects that it knows should be deleted
|
||||
* Updated plugin to correctly listen to `Plugin.Unloading` when installing or uninstalling new plugins
|
||||
|
||||
## [0.5.0 Alpha 5](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.5) (March 1, 2019)
|
||||
* Upgraded core dependencies, which improves compatibility for lots of instance types
|
||||
* Upgraded from `rbx_tree` 0.2.0 to `rbx_dom_weak` 1.0.0
|
||||
* Upgraded from `rbx_xml` 0.2.0 to `rbx_xml` 0.4.0
|
||||
* Upgraded from `rbx_binary` 0.2.0 to `rbx_binary` 0.4.0
|
||||
* Added support for non-primitive types in the Rojo plugin.
|
||||
* Types like `Color3` and `CFrame` can now be updated live!
|
||||
* Fixed plugin assets flashing in on first load ([#121](https://github.com/LPGhatguy/rojo/issues/121))
|
||||
* Changed Rojo's HTTP server from Rouille to Hyper, which reduced the release size by around a megabyte.
|
||||
* Added property type inference to projects, which makes specifying services a lot easier ([#130](https://github.com/LPGhatguy/rojo/pull/130))
|
||||
* Made error messages from invalid and missing files more user-friendly
|
||||
|
||||
## [0.5.0 Alpha 4](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.4) (February 8, 2019)
|
||||
* Added support for nested partitions ([#102](https://github.com/LPGhatguy/rojo/issues/102))
|
||||
* Added support for 'transmuting' partitions ([#112](https://github.com/LPGhatguy/rojo/issues/112))
|
||||
|
||||
1479
Cargo.lock
generated
1479
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"server",
|
||||
"rojo-e2e",
|
||||
]
|
||||
]
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
@@ -62,7 +62,7 @@ If you use a plugin that _isn't_ Rojo for syncing code, open an issue and let me
|
||||
## Contributing
|
||||
Pull requests are welcome!
|
||||
|
||||
Rojo supports Rust 1.31.1 and newer. Any changes to the minimum required compiler version require a _minor_ version bump.
|
||||
Rojo supports Rust 1.32 and newer. Any changes to the minimum required compiler version require a _minor_ version bump.
|
||||
|
||||
## License
|
||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||
39
design.gv
39
design.gv
@@ -1,39 +0,0 @@
|
||||
digraph G {
|
||||
graph [
|
||||
ranksep = "0.7",
|
||||
nodesep = "1.0",
|
||||
];
|
||||
node [
|
||||
fontname = "Hack",
|
||||
shape = "record",
|
||||
];
|
||||
|
||||
roblox_studio -> plugin [dir = "both"];
|
||||
plugin -> web_server [style = "dashed", dir = "both"];
|
||||
|
||||
web_server -> session;
|
||||
|
||||
session -> rbx_session;
|
||||
session -> fs_watcher;
|
||||
session -> message_queue;
|
||||
|
||||
fs_watcher -> imfs [weight = "10"];
|
||||
fs_watcher -> rbx_session [constraint = "false"];
|
||||
|
||||
imfs -> fs;
|
||||
|
||||
rbx_session -> imfs;
|
||||
rbx_session -> middlewares [weight = "10"];
|
||||
rbx_session -> message_queue [constraint = "false"];
|
||||
|
||||
plugin [label = "Studio Plugin"];
|
||||
roblox_studio [label = "Roblox Studio"];
|
||||
fs [label = "Filesystem"];
|
||||
fs_watcher [label = "Filesystem Watcher"];
|
||||
session [label = "Session"];
|
||||
web_server [label = "Web API"];
|
||||
imfs [label = "In-Memory Filesystem"];
|
||||
rbx_session [label = "RbxSession"];
|
||||
message_queue [label = "MessageQueue"];
|
||||
middlewares [label = "Middlewares"];
|
||||
}
|
||||
@@ -28,13 +28,13 @@ Now that we have a project, one thing we can do is build a Roblox place file for
|
||||
All we have to do is call `rojo build`:
|
||||
|
||||
```sh
|
||||
rojo build -o MyNewProject.rbxl
|
||||
rojo build -o MyNewProject.rbxlx
|
||||
```
|
||||
|
||||
If you open `MyNewProject.rbxl` in Roblox Studio now, you should see a `Folder` containing a `ModuleScript` under `ReplicatedStorage`!
|
||||
If you open `MyNewProject.rbxlx` in Roblox Studio now, you should see a `Folder` containing a `ModuleScript` under `ReplicatedStorage`!
|
||||
|
||||
!!! info
|
||||
To generate an XML place file instead, like if you're checking the place file into version control, just use `rbxlx` as the extension on the output file instead.
|
||||
To generate a binary place file instead, use `rbxl`. Note that support for binary model/place files (`rbxm` and `rbxl`) is very limited in Rojo presently.
|
||||
|
||||
## Live-Syncing into Studio
|
||||
Building a place file is great for the initial build, but for actively working on your place, you'll want something quicker.
|
||||
|
||||
@@ -25,13 +25,13 @@ If you have Rust installed, the easiest way to get Rojo is with Cargo!
|
||||
To install the latest 0.5.0 alpha, use:
|
||||
|
||||
```sh
|
||||
cargo install rojo --version 0.5.0-alpha.3
|
||||
cargo install rojo --version 0.5.0-alpha.11
|
||||
```
|
||||
|
||||
## Installing the Plugin
|
||||
|
||||
### Installing from GitHub
|
||||
The Rojo Roblox Studio plugin is available available from Rojo's [GitHub Releases page](https://github.com/LPGhatguy/rojo/releases).
|
||||
The Rojo Roblox Studio plugin is available from Rojo's [GitHub Releases page](https://github.com/LPGhatguy/rojo/releases).
|
||||
|
||||
Download the attached `rbxm` file and put it into your Roblox Studio plugins folder. You can find that folder by pressing **Plugins Folder** from your Plugins toolbar in Roblox Studio:
|
||||
|
||||
@@ -42,4 +42,4 @@ Download the attached `rbxm` file and put it into your Roblox Studio plugins fol
|
||||
Visit [Rojo's Roblox.com Plugin page](https://www.roblox.com/library/1997686364/Rojo-0-5-0-alpha-3) in Roblox Studio and press **Install**.
|
||||
|
||||
## 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!
|
||||
If you use Visual Studio Code, 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 sync files and start/stop the Rojo server!
|
||||
@@ -86,6 +86,6 @@ It would turn into instances in this shape:
|
||||
## Binary and XML Models
|
||||
Rojo supports both binary (`.rbxm`) and XML (`.rbxmx`) models generated by Roblox Studio or another tool.
|
||||
|
||||
Not all property types are supported for all formats!
|
||||
Support for the `rbxmx` is very good, while support for `rbxm` is still very early, buggy, and lacking features.
|
||||
|
||||
For a rundown of supported types, check out [rbx_tree's type coverage chart](https://github.com/LPGhatguy/rbx-tree#property-type-coverage).
|
||||
For a rundown of supported types, check out [rbx-dom's type coverage chart](https://github.com/LPGhatguy/rbx-dom#property-type-coverage).
|
||||
@@ -13,6 +13,7 @@ stds.roblox = {
|
||||
|
||||
-- Types
|
||||
"Vector2", "Vector3",
|
||||
"Vector2int16", "Vector3int16",
|
||||
"Color3",
|
||||
"UDim", "UDim2",
|
||||
"Rect",
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
"$path": "src"
|
||||
},
|
||||
"Roact": {
|
||||
"$path": "modules/roact/lib"
|
||||
},
|
||||
"Rodux": {
|
||||
"$path": "modules/rodux/lib"
|
||||
},
|
||||
"RoactRodux": {
|
||||
"$path": "modules/roact-rodux/lib"
|
||||
"$path": "modules/roact/src"
|
||||
},
|
||||
"Promise": {
|
||||
"$path": "modules/promise/lib"
|
||||
},
|
||||
"t": {
|
||||
"$path": "modules/t/lib"
|
||||
},
|
||||
"RbxDom": {
|
||||
"$path": "modules/rbx-dom/rbx_dom_lua/src"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
plugin/modules/rbx-dom
Submodule
1
plugin/modules/rbx-dom
Submodule
Submodule plugin/modules/rbx-dom added at ac129152f2
Submodule plugin/modules/roact updated: 8da5b29805...2fb60ea648
Submodule plugin/modules/roact-rodux deleted from 5d2e6885fc
Submodule plugin/modules/rodux deleted from 862f1c769a
1
plugin/modules/t
Submodule
1
plugin/modules/t
Submodule
Submodule plugin/modules/t added at f643b50682
@@ -13,16 +13,13 @@
|
||||
"$path": "src"
|
||||
},
|
||||
"Roact": {
|
||||
"$path": "modules/roact/lib"
|
||||
},
|
||||
"Rodux": {
|
||||
"$path": "modules/rodux/lib"
|
||||
},
|
||||
"RoactRodux": {
|
||||
"$path": "modules/roact-rodux/lib"
|
||||
"$path": "modules/roact/src"
|
||||
},
|
||||
"Promise": {
|
||||
"$path": "modules/promise/lib"
|
||||
},
|
||||
"t": {
|
||||
"$path": "modules/t/lib"
|
||||
}
|
||||
},
|
||||
"TestEZ": {
|
||||
@@ -40,8 +37,8 @@
|
||||
}
|
||||
},
|
||||
|
||||
"TestService": {
|
||||
"$className": "TestService",
|
||||
"ServerScriptService": {
|
||||
"$className": "ServerScriptService",
|
||||
|
||||
"TestBootstrap": {
|
||||
"$path": "testBootstrap.server.lua"
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "rojo",
|
||||
"servePort": 8000,
|
||||
"partitions": {
|
||||
"plugin": {
|
||||
"path": "src",
|
||||
"target": "ReplicatedStorage.Rojo.Plugin"
|
||||
},
|
||||
"modules/roact": {
|
||||
"path": "modules/roact/lib",
|
||||
"target": "ReplicatedStorage.Rojo.Roact"
|
||||
},
|
||||
"modules/rodux": {
|
||||
"path": "modules/rodux/lib",
|
||||
"target": "ReplicatedStorage.Rojo.Rodux"
|
||||
},
|
||||
"modules/roact-rodux": {
|
||||
"path": "modules/roact-rodux/lib",
|
||||
"target": "ReplicatedStorage.Rojo.RoactRodux"
|
||||
},
|
||||
"modules/promise": {
|
||||
"path": "modules/promise/lib",
|
||||
"target": "ReplicatedStorage.Rojo.Promise"
|
||||
},
|
||||
"modules/testez": {
|
||||
"path": "modules/testez/lib",
|
||||
"target": "ReplicatedStorage.TestEZ"
|
||||
},
|
||||
"tests": {
|
||||
"path": "testBootstrap.server.lua",
|
||||
"target": "TestService.testBootstrap"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
local sheetAsset = "rbxassetid://2738712459"
|
||||
|
||||
local Assets = {
|
||||
Sprites = {
|
||||
WhiteCross = {
|
||||
asset = sheetAsset,
|
||||
asset = "rbxassetid://2738712459",
|
||||
offset = Vector2.new(190, 318),
|
||||
size = Vector2.new(18, 18),
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ local Config = require(Plugin.Config)
|
||||
local Version = require(Plugin.Version)
|
||||
local Logging = require(Plugin.Logging)
|
||||
local DevSettings = require(Plugin.DevSettings)
|
||||
local preloadAssets = require(Plugin.preloadAssets)
|
||||
|
||||
local ConnectPanel = require(Plugin.Components.ConnectPanel)
|
||||
local ConnectionActivePanel = require(Plugin.Components.ConnectionActivePanel)
|
||||
@@ -79,7 +80,8 @@ function App:init()
|
||||
end
|
||||
|
||||
function App:render()
|
||||
local children
|
||||
-- FIXME: https://github.com/Roblox/roact/issues/209
|
||||
local children = {}
|
||||
|
||||
if self.state.sessionStatus == SessionStatus.Connected then
|
||||
children = {
|
||||
@@ -177,6 +179,15 @@ function App:didMount()
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
preloadAssets()
|
||||
end
|
||||
|
||||
function App:willUnmount()
|
||||
if self.currentSession ~= nil then
|
||||
self.currentSession:disconnect()
|
||||
self.currentSession = nil
|
||||
end
|
||||
end
|
||||
|
||||
function App:didUpdate()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
return {
|
||||
codename = "Epiphany",
|
||||
version = {0, 5, 0, "-alpha.4"},
|
||||
version = {0, 5, 0, "-alpha.11"},
|
||||
expectedServerVersionString = "0.5.0 or newer",
|
||||
protocolVersion = 2,
|
||||
defaultHost = "localhost",
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
local Config = require(script.Parent.Config)
|
||||
|
||||
local Environment = {
|
||||
User = "User",
|
||||
Dev = "Dev",
|
||||
Test = "Test",
|
||||
}
|
||||
|
||||
local VALUES = {
|
||||
LogLevel = {
|
||||
type = "IntValue",
|
||||
defaultUserValue = 2,
|
||||
defaultDevValue = 3,
|
||||
values = {
|
||||
[Environment.User] = 2,
|
||||
[Environment.Dev] = 3,
|
||||
[Environment.Test] = 3,
|
||||
},
|
||||
},
|
||||
TypecheckingEnabled = {
|
||||
type = "BoolValue",
|
||||
values = {
|
||||
[Environment.User] = false,
|
||||
[Environment.Dev] = true,
|
||||
[Environment.Test] = true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -42,7 +59,9 @@ local function setStoredValue(name, kind, value)
|
||||
object.Value = value
|
||||
end
|
||||
|
||||
local function createAllValues()
|
||||
local function createAllValues(environment)
|
||||
assert(Environment[environment] ~= nil, "Invalid environment")
|
||||
|
||||
valueContainer = getValueContainer()
|
||||
|
||||
if valueContainer == nil then
|
||||
@@ -52,20 +71,57 @@ local function createAllValues()
|
||||
end
|
||||
|
||||
for name, value in pairs(VALUES) do
|
||||
setStoredValue(name, value.type, value.defaultDevValue)
|
||||
setStoredValue(name, value.type, value.values[environment])
|
||||
end
|
||||
end
|
||||
|
||||
_G[("ROJO_%s_DEV_CREATE"):format(Config.codename:upper())] = createAllValues
|
||||
local function getValue(name)
|
||||
assert(VALUES[name] ~= nil, "Invalid DevSettings name")
|
||||
|
||||
local stored = getStoredValue(name)
|
||||
|
||||
if stored ~= nil then
|
||||
return stored
|
||||
end
|
||||
|
||||
return VALUES[name].values[Environment.User]
|
||||
end
|
||||
|
||||
local DevSettings = {}
|
||||
|
||||
function DevSettings:createDevSettings()
|
||||
createAllValues(Environment.Dev)
|
||||
end
|
||||
|
||||
function DevSettings:createTestSettings()
|
||||
createAllValues(Environment.Test)
|
||||
end
|
||||
|
||||
function DevSettings:hasChangedValues()
|
||||
return valueContainer ~= nil
|
||||
end
|
||||
|
||||
function DevSettings:resetValues()
|
||||
if valueContainer then
|
||||
valueContainer:Destroy()
|
||||
valueContainer = nil
|
||||
end
|
||||
end
|
||||
|
||||
function DevSettings:isEnabled()
|
||||
return valueContainer ~= nil
|
||||
end
|
||||
|
||||
function DevSettings:getLogLevel()
|
||||
return getStoredValue("LogLevel") or VALUES.LogLevel.defaultUserValue
|
||||
return getValue("LogLevel")
|
||||
end
|
||||
|
||||
function DevSettings:shouldTypecheck()
|
||||
return getValue("TypecheckingEnabled")
|
||||
end
|
||||
|
||||
function _G.ROJO_DEV_CREATE()
|
||||
DevSettings:createDevSettings()
|
||||
end
|
||||
|
||||
return DevSettings
|
||||
81
plugin/src/InstanceMap.lua
Normal file
81
plugin/src/InstanceMap.lua
Normal file
@@ -0,0 +1,81 @@
|
||||
local Logging = require(script.Parent.Logging)
|
||||
|
||||
--[[
|
||||
A bidirectional map between instance IDs and Roblox instances. It lets us
|
||||
keep track of every instance we know about.
|
||||
|
||||
TODO: Track ancestry to catch when stuff moves?
|
||||
]]
|
||||
local InstanceMap = {}
|
||||
InstanceMap.__index = InstanceMap
|
||||
|
||||
function InstanceMap.new()
|
||||
local self = {
|
||||
fromIds = {},
|
||||
fromInstances = {},
|
||||
}
|
||||
|
||||
return setmetatable(self, InstanceMap)
|
||||
end
|
||||
|
||||
function InstanceMap:insert(id, instance)
|
||||
self.fromIds[id] = instance
|
||||
self.fromInstances[instance] = id
|
||||
end
|
||||
|
||||
function InstanceMap:removeId(id)
|
||||
local instance = self.fromIds[id]
|
||||
|
||||
if instance ~= nil then
|
||||
self.fromIds[id] = nil
|
||||
self.fromInstances[instance] = nil
|
||||
else
|
||||
Logging.warn("Attempted to remove nonexistant ID %s", tostring(id))
|
||||
end
|
||||
end
|
||||
|
||||
function InstanceMap:removeInstance(instance)
|
||||
local id = self.fromInstances[instance]
|
||||
|
||||
if id ~= nil then
|
||||
self.fromInstances[instance] = nil
|
||||
self.fromIds[id] = nil
|
||||
else
|
||||
Logging.warn("Attempted to remove nonexistant instance %s", tostring(instance))
|
||||
end
|
||||
end
|
||||
|
||||
function InstanceMap:destroyInstance(instance)
|
||||
local id = self.fromInstances[instance]
|
||||
|
||||
if id ~= nil then
|
||||
self:destroyId(id)
|
||||
else
|
||||
Logging.warn("Attempted to destroy untracked instance %s", tostring(instance))
|
||||
end
|
||||
end
|
||||
|
||||
function InstanceMap:destroyId(id)
|
||||
local instance = self.fromIds[id]
|
||||
self:removeId(id)
|
||||
|
||||
if instance ~= nil then
|
||||
local descendantsToDestroy = {}
|
||||
|
||||
for otherInstance in pairs(self.fromInstances) do
|
||||
if otherInstance:IsDescendantOf(instance) then
|
||||
table.insert(descendantsToDestroy, otherInstance)
|
||||
end
|
||||
end
|
||||
|
||||
for _, otherInstance in ipairs(descendantsToDestroy) do
|
||||
self:removeInstance(otherInstance)
|
||||
end
|
||||
|
||||
instance:Destroy()
|
||||
else
|
||||
Logging.warn("Attempted to destroy nonexistant ID %s", tostring(id))
|
||||
end
|
||||
end
|
||||
|
||||
return InstanceMap
|
||||
@@ -1,100 +1,15 @@
|
||||
local t = require(script.Parent.Parent.t)
|
||||
|
||||
local InstanceMap = require(script.Parent.InstanceMap)
|
||||
local Logging = require(script.Parent.Logging)
|
||||
local setCanonicalProperty = require(script.Parent.setCanonicalProperty)
|
||||
local rojoValueToRobloxValue = require(script.Parent.rojoValueToRobloxValue)
|
||||
local Types = require(script.Parent.Types)
|
||||
|
||||
local function makeInstanceMap()
|
||||
local self = {
|
||||
fromIds = {},
|
||||
fromInstances = {},
|
||||
}
|
||||
|
||||
function self:insert(id, instance)
|
||||
self.fromIds[id] = instance
|
||||
self.fromInstances[instance] = id
|
||||
end
|
||||
|
||||
function self:removeId(id)
|
||||
local instance = self.fromIds[id]
|
||||
|
||||
if instance ~= nil then
|
||||
self.fromIds[id] = nil
|
||||
self.fromInstances[instance] = nil
|
||||
else
|
||||
Logging.warn("Attempted to remove nonexistant ID %s", tostring(id))
|
||||
end
|
||||
end
|
||||
|
||||
function self:removeInstance(instance)
|
||||
local id = self.fromInstances[instance]
|
||||
|
||||
if id ~= nil then
|
||||
self.fromInstances[instance] = nil
|
||||
self.fromIds[id] = nil
|
||||
else
|
||||
Logging.warn("Attempted to remove nonexistant instance %s", tostring(instance))
|
||||
end
|
||||
end
|
||||
|
||||
function self:destroyId(id)
|
||||
local instance = self.fromIds[id]
|
||||
self:removeId(id)
|
||||
|
||||
if instance ~= nil then
|
||||
local descendantsToDestroy = {}
|
||||
|
||||
for otherInstance in pairs(self.fromInstances) do
|
||||
if otherInstance:IsDescendantOf(instance) then
|
||||
table.insert(descendantsToDestroy, otherInstance)
|
||||
end
|
||||
end
|
||||
|
||||
for _, otherInstance in ipairs(descendantsToDestroy) do
|
||||
self:removeInstance(otherInstance)
|
||||
end
|
||||
|
||||
instance:Destroy()
|
||||
else
|
||||
Logging.warn("Attempted to destroy nonexistant ID %s", tostring(id))
|
||||
end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
local function setProperty(instance, key, value)
|
||||
-- The 'Contents' property of LocalizationTable isn't directly exposed, but
|
||||
-- has corresponding (deprecated) getters and setters.
|
||||
if key == "Contents" and instance.ClassName == "LocalizationTable" then
|
||||
instance:SetContents(value)
|
||||
return
|
||||
end
|
||||
|
||||
-- If we don't have permissions to access this value at all, we can skip it.
|
||||
local readSuccess, existingValue = pcall(function()
|
||||
return instance[key]
|
||||
local function setParent(instance, newParent)
|
||||
pcall(function()
|
||||
instance.Parent = newParent
|
||||
end)
|
||||
|
||||
if not readSuccess then
|
||||
-- An error will be thrown if there was a permission issue or if the
|
||||
-- property doesn't exist. In the latter case, we should tell the user
|
||||
-- because it's probably their fault.
|
||||
if existingValue:find("lacking permission") then
|
||||
Logging.trace("Permission error reading property %s on class %s", tostring(key), instance.ClassName)
|
||||
return
|
||||
else
|
||||
error(("Invalid property %s on class %s: %s"):format(tostring(key), instance.ClassName, existingValue), 2)
|
||||
end
|
||||
end
|
||||
|
||||
local writeSuccess, err = pcall(function()
|
||||
if existingValue ~= value then
|
||||
instance[key] = value
|
||||
end
|
||||
end)
|
||||
|
||||
if not writeSuccess then
|
||||
error(("Cannot set property %s on class %s: %s"):format(tostring(key), instance.ClassName, err), 2)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local Reconciler = {}
|
||||
@@ -102,7 +17,7 @@ Reconciler.__index = Reconciler
|
||||
|
||||
function Reconciler.new()
|
||||
local self = {
|
||||
instanceMap = makeInstanceMap(),
|
||||
instanceMap = InstanceMap.new(),
|
||||
}
|
||||
|
||||
return setmetatable(self, Reconciler)
|
||||
@@ -118,11 +33,18 @@ function Reconciler:applyUpdate(requestedIds, virtualInstancesById)
|
||||
end
|
||||
end
|
||||
|
||||
local reconcileSchema = Types.ifEnabled(t.tuple(
|
||||
t.map(t.string, Types.VirtualInstance),
|
||||
t.string,
|
||||
t.Instance
|
||||
))
|
||||
--[[
|
||||
Update an existing instance, including its properties and children, to match
|
||||
the given information.
|
||||
]]
|
||||
function Reconciler:reconcile(virtualInstancesById, id, instance)
|
||||
assert(reconcileSchema(virtualInstancesById, id, instance))
|
||||
|
||||
local virtualInstance = virtualInstancesById[id]
|
||||
|
||||
-- If an instance changes ClassName, we assume it's very different. That's
|
||||
@@ -137,10 +59,10 @@ function Reconciler:reconcile(virtualInstancesById, id, instance)
|
||||
self.instanceMap:insert(id, instance)
|
||||
|
||||
-- Some instances don't like being named, even if their name already matches
|
||||
setProperty(instance, "Name", virtualInstance.Name)
|
||||
setCanonicalProperty(instance, "Name", virtualInstance.Name)
|
||||
|
||||
for key, value in pairs(virtualInstance.Properties) do
|
||||
setProperty(instance, key, value.Value)
|
||||
setCanonicalProperty(instance, key, rojoValueToRobloxValue(value))
|
||||
end
|
||||
|
||||
local existingChildren = instance:GetChildren()
|
||||
@@ -175,10 +97,17 @@ function Reconciler:reconcile(virtualInstancesById, id, instance)
|
||||
end
|
||||
end
|
||||
|
||||
if self:__shouldClearUnknownInstances(virtualInstance) then
|
||||
for existingChildInstance in pairs(unvisitedExistingChildren) do
|
||||
self.instanceMap:removeInstance(existingChildInstance)
|
||||
existingChildInstance:Destroy()
|
||||
local shouldClearUnknown = self:__shouldClearUnknownChildren(virtualInstance)
|
||||
|
||||
for existingChildInstance in pairs(unvisitedExistingChildren) do
|
||||
local childId = self.instanceMap.fromInstances[existingChildInstance]
|
||||
|
||||
if childId == nil then
|
||||
if shouldClearUnknown then
|
||||
existingChildInstance:Destroy()
|
||||
end
|
||||
else
|
||||
self.instanceMap:destroyInstance(existingChildInstance)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -194,16 +123,13 @@ function Reconciler:reconcile(virtualInstancesById, id, instance)
|
||||
|
||||
-- Some instances, like services, don't like having their Parent
|
||||
-- property poked, even if we're setting it to the same value.
|
||||
setProperty(instance, "Parent", parent)
|
||||
if instance.Parent ~= parent then
|
||||
instance.Parent = parent
|
||||
end
|
||||
setParent(instance, parent)
|
||||
end
|
||||
|
||||
return instance
|
||||
end
|
||||
|
||||
function Reconciler:__shouldClearUnknownInstances(virtualInstance)
|
||||
function Reconciler:__shouldClearUnknownChildren(virtualInstance)
|
||||
if virtualInstance.Metadata ~= nil then
|
||||
return not virtualInstance.Metadata.ignoreUnknownInstances
|
||||
else
|
||||
@@ -211,29 +137,44 @@ function Reconciler:__shouldClearUnknownInstances(virtualInstance)
|
||||
end
|
||||
end
|
||||
|
||||
local reifySchema = Types.ifEnabled(t.tuple(
|
||||
t.map(t.string, Types.VirtualInstance),
|
||||
t.string,
|
||||
t.Instance
|
||||
))
|
||||
|
||||
function Reconciler:__reify(virtualInstancesById, id, parent)
|
||||
assert(reifySchema(virtualInstancesById, id, parent))
|
||||
|
||||
local virtualInstance = virtualInstancesById[id]
|
||||
|
||||
local instance = Instance.new(virtualInstance.ClassName)
|
||||
|
||||
for key, value in pairs(virtualInstance.Properties) do
|
||||
-- TODO: Branch on value.Type
|
||||
setProperty(instance, key, value.Value)
|
||||
setCanonicalProperty(instance, key, rojoValueToRobloxValue(value))
|
||||
end
|
||||
|
||||
instance.Name = virtualInstance.Name
|
||||
setCanonicalProperty(instance, "Name", virtualInstance.Name)
|
||||
|
||||
for _, childId in ipairs(virtualInstance.Children) do
|
||||
self:__reify(virtualInstancesById, childId, instance)
|
||||
end
|
||||
|
||||
setProperty(instance, "Parent", parent)
|
||||
setParent(instance, parent)
|
||||
self.instanceMap:insert(id, instance)
|
||||
|
||||
return instance
|
||||
end
|
||||
|
||||
local applyUpdatePieceSchema = Types.ifEnabled(t.tuple(
|
||||
t.string,
|
||||
t.map(t.string, t.boolean),
|
||||
t.map(t.string, Types.VirtualInstance)
|
||||
))
|
||||
|
||||
function Reconciler:__applyUpdatePiece(id, visitedIds, virtualInstancesById)
|
||||
assert(applyUpdatePieceSchema(id, visitedIds, virtualInstancesById))
|
||||
|
||||
if visitedIds[id] then
|
||||
return
|
||||
end
|
||||
|
||||
218
plugin/src/Reconciler.spec.lua
Normal file
218
plugin/src/Reconciler.spec.lua
Normal file
@@ -0,0 +1,218 @@
|
||||
local Reconciler = require(script.Parent.Reconciler)
|
||||
|
||||
return function()
|
||||
it("should leave instances alone if there's nothing specified", function()
|
||||
local instance = Instance.new("Folder")
|
||||
instance.Name = "TestFolder"
|
||||
|
||||
local instanceId = "test-id"
|
||||
local virtualInstancesById = {
|
||||
[instanceId] = {
|
||||
Name = "TestFolder",
|
||||
ClassName = "Folder",
|
||||
Children = {},
|
||||
Properties = {},
|
||||
},
|
||||
}
|
||||
|
||||
local reconciler = Reconciler.new()
|
||||
reconciler:reconcile(virtualInstancesById, instanceId, instance)
|
||||
end)
|
||||
|
||||
it("should assign names from virtual instances", function()
|
||||
local instance = Instance.new("Folder")
|
||||
instance.Name = "InitialName"
|
||||
|
||||
local instanceId = "test-id"
|
||||
local virtualInstancesById = {
|
||||
[instanceId] = {
|
||||
Name = "NewName",
|
||||
ClassName = "Folder",
|
||||
Children = {},
|
||||
Properties = {},
|
||||
},
|
||||
}
|
||||
|
||||
local reconciler = Reconciler.new()
|
||||
reconciler:reconcile(virtualInstancesById, instanceId, instance)
|
||||
|
||||
expect(instance.Name).to.equal("NewName")
|
||||
end)
|
||||
|
||||
it("should assign properties from virtual instances", function()
|
||||
local instance = Instance.new("IntValue")
|
||||
instance.Name = "TestValue"
|
||||
instance.Value = 5
|
||||
|
||||
local instanceId = "test-id"
|
||||
local virtualInstancesById = {
|
||||
[instanceId] = {
|
||||
Name = "TestValue",
|
||||
ClassName = "IntValue",
|
||||
Children = {},
|
||||
Properties = {
|
||||
Value = {
|
||||
Type = "Int32",
|
||||
Value = 9
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local reconciler = Reconciler.new()
|
||||
reconciler:reconcile(virtualInstancesById, instanceId, instance)
|
||||
|
||||
expect(instance.Value).to.equal(9)
|
||||
end)
|
||||
|
||||
it("should wipe unknown children by default", function()
|
||||
local parent = Instance.new("Folder")
|
||||
parent.Name = "Parent"
|
||||
|
||||
local child = Instance.new("Folder")
|
||||
child.Name = "Child"
|
||||
|
||||
local parentId = "test-id"
|
||||
local virtualInstancesById = {
|
||||
[parentId] = {
|
||||
Name = "Parent",
|
||||
ClassName = "Folder",
|
||||
Children = {},
|
||||
Properties = {},
|
||||
},
|
||||
}
|
||||
|
||||
local reconciler = Reconciler.new()
|
||||
reconciler:reconcile(virtualInstancesById, parentId, parent)
|
||||
|
||||
expect(#parent:GetChildren()).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should preserve unknown children if ignoreUnknownInstances is set", function()
|
||||
local parent = Instance.new("Folder")
|
||||
parent.Name = "Parent"
|
||||
|
||||
local child = Instance.new("Folder")
|
||||
child.Parent = parent
|
||||
child.Name = "Child"
|
||||
|
||||
local parentId = "test-id"
|
||||
local virtualInstancesById = {
|
||||
[parentId] = {
|
||||
Name = "Parent",
|
||||
ClassName = "Folder",
|
||||
Children = {},
|
||||
Properties = {},
|
||||
Metadata = {
|
||||
ignoreUnknownInstances = true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local reconciler = Reconciler.new()
|
||||
reconciler:reconcile(virtualInstancesById, parentId, parent)
|
||||
|
||||
expect(child.Parent).to.equal(parent)
|
||||
expect(#parent:GetChildren()).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should remove known removed children", function()
|
||||
local parent = Instance.new("Folder")
|
||||
parent.Name = "Parent"
|
||||
|
||||
local child = Instance.new("Folder")
|
||||
child.Parent = parent
|
||||
child.Name = "Child"
|
||||
|
||||
local parentId = "parent-id"
|
||||
local childId = "child-id"
|
||||
|
||||
local reconciler = Reconciler.new()
|
||||
|
||||
local virtualInstancesById = {
|
||||
[parentId] = {
|
||||
Name = "Parent",
|
||||
ClassName = "Folder",
|
||||
Children = {childId},
|
||||
Properties = {},
|
||||
},
|
||||
[childId] = {
|
||||
Name = "Child",
|
||||
ClassName = "Folder",
|
||||
Children = {},
|
||||
Properties = {},
|
||||
},
|
||||
}
|
||||
reconciler:reconcile(virtualInstancesById, parentId, parent)
|
||||
|
||||
expect(child.Parent).to.equal(parent)
|
||||
expect(#parent:GetChildren()).to.equal(1)
|
||||
|
||||
local newVirtualInstances = {
|
||||
[parentId] = {
|
||||
Name = "Parent",
|
||||
ClassName = "Folder",
|
||||
Children = {},
|
||||
Properties = {},
|
||||
},
|
||||
[childId] = nil,
|
||||
}
|
||||
reconciler:reconcile(newVirtualInstances, parentId, parent)
|
||||
|
||||
expect(child.Parent).to.equal(nil)
|
||||
expect(#parent:GetChildren()).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should remove known removed children if ignoreUnknownInstances is set", function()
|
||||
local parent = Instance.new("Folder")
|
||||
parent.Name = "Parent"
|
||||
|
||||
local child = Instance.new("Folder")
|
||||
child.Parent = parent
|
||||
child.Name = "Child"
|
||||
|
||||
local parentId = "parent-id"
|
||||
local childId = "child-id"
|
||||
|
||||
local reconciler = Reconciler.new()
|
||||
|
||||
local virtualInstancesById = {
|
||||
[parentId] = {
|
||||
Name = "Parent",
|
||||
ClassName = "Folder",
|
||||
Children = {childId},
|
||||
Properties = {},
|
||||
Metadata = {
|
||||
ignoreUnknownInstances = true,
|
||||
},
|
||||
},
|
||||
[childId] = {
|
||||
Name = "Child",
|
||||
ClassName = "Folder",
|
||||
Children = {},
|
||||
Properties = {},
|
||||
},
|
||||
}
|
||||
reconciler:reconcile(virtualInstancesById, parentId, parent)
|
||||
|
||||
expect(child.Parent).to.equal(parent)
|
||||
expect(#parent:GetChildren()).to.equal(1)
|
||||
|
||||
local newVirtualInstances = {
|
||||
[parentId] = {
|
||||
Name = "Parent",
|
||||
ClassName = "Folder",
|
||||
Children = {},
|
||||
Properties = {},
|
||||
Metadata = {
|
||||
ignoreUnknownInstances = true,
|
||||
},
|
||||
},
|
||||
[childId] = nil,
|
||||
}
|
||||
reconciler:reconcile(newVirtualInstances, parentId, parent)
|
||||
|
||||
expect(child.Parent).to.equal(nil)
|
||||
expect(#parent:GetChildren()).to.equal(0)
|
||||
end)
|
||||
end
|
||||
36
plugin/src/Types.lua
Normal file
36
plugin/src/Types.lua
Normal file
@@ -0,0 +1,36 @@
|
||||
local t = require(script.Parent.Parent.t)
|
||||
|
||||
local DevSettings = require(script.Parent.DevSettings)
|
||||
|
||||
local VirtualValue = t.interface({
|
||||
Type = t.string,
|
||||
Value = t.optional(t.any),
|
||||
})
|
||||
|
||||
local VirtualMetadata = t.interface({
|
||||
ignoreUnknownInstances = t.optional(t.boolean),
|
||||
})
|
||||
|
||||
local VirtualInstance = t.interface({
|
||||
Name = t.string,
|
||||
ClassName = t.string,
|
||||
Properties = t.map(t.string, VirtualValue),
|
||||
Metadata = t.optional(VirtualMetadata)
|
||||
})
|
||||
|
||||
local function ifEnabled(innerCheck)
|
||||
return function(...)
|
||||
if DevSettings:shouldTypecheck() then
|
||||
return innerCheck(...)
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
ifEnabled = ifEnabled,
|
||||
VirtualInstance = VirtualInstance,
|
||||
VirtualMetadata = VirtualMetadata,
|
||||
VirtualValue = VirtualValue,
|
||||
}
|
||||
@@ -4,16 +4,14 @@ end
|
||||
|
||||
local Roact = require(script.Parent.Roact)
|
||||
|
||||
Roact.setGlobalConfig({
|
||||
elementTracing = true,
|
||||
})
|
||||
|
||||
local App = require(script.Components.App)
|
||||
|
||||
local app = Roact.createElement(App, {
|
||||
plugin = plugin,
|
||||
})
|
||||
|
||||
Roact.mount(app, game:GetService("CoreGui"), "Rojo UI")
|
||||
local tree = Roact.mount(app, game:GetService("CoreGui"), "Rojo UI")
|
||||
|
||||
-- TODO: Detect another instance of Rojo coming online and shut down this one.
|
||||
plugin.Unloading:Connect(function()
|
||||
Roact.unmount(tree)
|
||||
end)
|
||||
28
plugin/src/preloadAssets.lua
Normal file
28
plugin/src/preloadAssets.lua
Normal file
@@ -0,0 +1,28 @@
|
||||
local ContentProvider = game:GetService("ContentProvider")
|
||||
|
||||
local Logging = require(script.Parent.Logging)
|
||||
local Assets = require(script.Parent.Assets)
|
||||
|
||||
local function preloadAssets()
|
||||
local contentUrls = {}
|
||||
|
||||
for _, sprite in pairs(Assets.Sprites) do
|
||||
table.insert(contentUrls, sprite.asset)
|
||||
end
|
||||
|
||||
for _, slice in pairs(Assets.Slices) do
|
||||
table.insert(contentUrls, slice.asset)
|
||||
end
|
||||
|
||||
for _, url in pairs(Assets.Images) do
|
||||
table.insert(contentUrls, url)
|
||||
end
|
||||
|
||||
Logging.trace("Preloading assets: %s", table.concat(contentUrls, ", "))
|
||||
|
||||
coroutine.wrap(function()
|
||||
ContentProvider:PreloadAsync(contentUrls)
|
||||
end)()
|
||||
end
|
||||
|
||||
return preloadAssets
|
||||
33
plugin/src/rojoValueToRobloxValue.lua
Normal file
33
plugin/src/rojoValueToRobloxValue.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
local RbxDom = require(script:FindFirstAncestor("Rojo").RbxDom)
|
||||
|
||||
local function rojoValueToRobloxValue(value)
|
||||
-- TODO: Manually decode this value by looking up its GUID The Rojo server
|
||||
-- doesn't give us valid ref values yet, so this isn't important yet.
|
||||
if value.Type == "Ref" then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- TODO: Remove this once rbx_dom_weak and rbx_dom_lua agree on encoding
|
||||
if value.Type == "BinaryString" then
|
||||
local actualValue = ""
|
||||
|
||||
for i = 1, #value.Value do
|
||||
actualValue = actualValue .. string.char(i)
|
||||
end
|
||||
|
||||
value = {
|
||||
Type = "BinaryString",
|
||||
Value = actualValue,
|
||||
}
|
||||
end
|
||||
|
||||
local success, decodedValue = RbxDom.EncodedValue.decode(value)
|
||||
|
||||
if not success then
|
||||
error(decodedValue, 2)
|
||||
end
|
||||
|
||||
return decodedValue
|
||||
end
|
||||
|
||||
return rojoValueToRobloxValue
|
||||
40
plugin/src/rojoValueToRobloxValue.spec.lua
Normal file
40
plugin/src/rojoValueToRobloxValue.spec.lua
Normal file
@@ -0,0 +1,40 @@
|
||||
local rojoValueToRobloxValue = require(script.Parent.rojoValueToRobloxValue)
|
||||
|
||||
return function()
|
||||
it("should convert primitives", function()
|
||||
local inputString = {
|
||||
Type = "String",
|
||||
Value = "Hello, world!",
|
||||
}
|
||||
|
||||
local inputFloat32 = {
|
||||
Type = "Float32",
|
||||
Value = 12341.512,
|
||||
}
|
||||
|
||||
expect(rojoValueToRobloxValue(inputString)).to.equal(inputString.Value)
|
||||
expect(rojoValueToRobloxValue(inputFloat32)).to.equal(inputFloat32.Value)
|
||||
end)
|
||||
|
||||
it("should convert properties with direct constructors", function()
|
||||
local inputColor3 = {
|
||||
Type = "Color3",
|
||||
Value = {0, 1, 0.5},
|
||||
}
|
||||
local outputColor3 = Color3.new(0, 1, 0.5)
|
||||
|
||||
local inputCFrame = {
|
||||
Type = "CFrame",
|
||||
Value = {
|
||||
1, 2, 3,
|
||||
4, 5, 6,
|
||||
7, 8, 9,
|
||||
10, 11, 12,
|
||||
},
|
||||
}
|
||||
local outputCFrame = CFrame.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
|
||||
|
||||
expect(rojoValueToRobloxValue(inputColor3)).to.equal(outputColor3)
|
||||
expect(rojoValueToRobloxValue(inputCFrame)).to.equal(outputCFrame)
|
||||
end)
|
||||
end
|
||||
37
plugin/src/setCanonicalProperty.lua
Normal file
37
plugin/src/setCanonicalProperty.lua
Normal file
@@ -0,0 +1,37 @@
|
||||
local RbxDom = require(script:FindFirstAncestor("Rojo").RbxDom)
|
||||
|
||||
local Logging = require(script.Parent.Logging)
|
||||
|
||||
--[[
|
||||
Attempts to set a property on the given instance.
|
||||
]]
|
||||
local function setCanonicalProperty(instance, key, value)
|
||||
if not RbxDom.CanonicalProperty.isScriptable(instance.ClassName, key) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- If we don't have permissions to access this value at all, we can skip it.
|
||||
local readSuccess, existingValue = RbxDom.CanonicalProperty.read(instance, key)
|
||||
|
||||
if not readSuccess then
|
||||
-- An error will be thrown if there was a permission issue or if the
|
||||
-- property doesn't exist. In the latter case, we should tell the user
|
||||
-- because it's probably their fault.
|
||||
if existingValue:find("lacking permission") then
|
||||
Logging.trace("Permission error reading property %s on class %s", tostring(key), instance.ClassName)
|
||||
return false
|
||||
else
|
||||
error(("Invalid property %s on class %s: %s"):format(tostring(key), instance.ClassName, existingValue), 2)
|
||||
end
|
||||
end
|
||||
|
||||
local writeSuccess, err = RbxDom.CanonicalProperty.write(instance, key, value)
|
||||
|
||||
if not writeSuccess then
|
||||
error(("Cannot set property %s on class %s: %s"):format(tostring(key), instance.ClassName, err), 2)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return setCanonicalProperty
|
||||
@@ -1,2 +1,19 @@
|
||||
local TestEZ = require(game.ReplicatedStorage.TestEZ)
|
||||
TestEZ.TestBootstrap:run(game.ReplicatedStorage.Rojo.plugin)
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local TestEZ = require(ReplicatedStorage.TestEZ)
|
||||
|
||||
local Rojo = ReplicatedStorage.Rojo
|
||||
|
||||
local DevSettings = require(Rojo.Plugin.DevSettings)
|
||||
|
||||
local setDevSettings = not DevSettings:hasChangedValues()
|
||||
|
||||
if setDevSettings then
|
||||
DevSettings:createTestSettings()
|
||||
end
|
||||
|
||||
TestEZ.TestBootstrap:run({Rojo.Plugin})
|
||||
|
||||
if setDevSettings then
|
||||
DevSettings:resetValues()
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
[package]
|
||||
name = "rojo-e2e"
|
||||
version = "0.1.0"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
|
||||
[dependencies]
|
||||
@@ -1,2 +0,0 @@
|
||||
# Rojo End-to-End
|
||||
This is a WIP test runner designed for Rojo. It will eventually start up the Rojo server and plugin and test functionality end-to-end.
|
||||
@@ -1,32 +0,0 @@
|
||||
use std::{
|
||||
path::Path,
|
||||
process::Command,
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let plugin_path = Path::new("../plugin");
|
||||
let server_path = Path::new("../server");
|
||||
let tests_path = Path::new("../tests");
|
||||
|
||||
let server = Command::new("cargo")
|
||||
.args(&["run", "--", "serve", "../test-projects/empty"])
|
||||
.current_dir(server_path)
|
||||
.spawn();
|
||||
|
||||
thread::sleep(Duration::from_millis(1000));
|
||||
|
||||
// TODO: Wait for server to start responding on the right port
|
||||
|
||||
let test_client = Command::new("lua")
|
||||
.args(&["runTest.lua", "tests/empty.lua"])
|
||||
.current_dir(plugin_path)
|
||||
.spawn();
|
||||
|
||||
thread::sleep(Duration::from_millis(300));
|
||||
|
||||
// TODO: Collect output from the client for success/failure?
|
||||
|
||||
println!("Dying!");
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
[package]
|
||||
name = "rojo"
|
||||
version = "0.5.0-alpha.4"
|
||||
version = "0.5.0-alpha.11"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
description = "A tool to create robust Roblox projects"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/LPGhatguy/rojo"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server-plugins = []
|
||||
|
||||
[lib]
|
||||
name = "librojo"
|
||||
path = "src/lib.rs"
|
||||
@@ -15,27 +19,25 @@ path = "src/lib.rs"
|
||||
name = "rojo"
|
||||
path = "src/bin.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
bundle-plugin = []
|
||||
|
||||
[dependencies]
|
||||
clap = "2.27"
|
||||
csv = "1.0"
|
||||
env_logger = "0.6"
|
||||
failure = "0.1.3"
|
||||
futures = "0.1"
|
||||
hyper = "0.12"
|
||||
log = "0.4"
|
||||
maplit = "1.0.1"
|
||||
notify = "4.0"
|
||||
rand = "0.4"
|
||||
rbx_binary = "0.2.0"
|
||||
rbx_tree = "0.2.0"
|
||||
rbx_xml = "0.2.0"
|
||||
rbx_binary = "0.4.0"
|
||||
rbx_dom_weak = "1.7.0"
|
||||
rbx_xml = "0.9.0"
|
||||
rbx_reflection = "3.0.384"
|
||||
regex = "1.0"
|
||||
reqwest = "0.9.5"
|
||||
rouille = "2.1"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
rlua = "0.16"
|
||||
ritz = "0.1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
uuid = { version = "0.7", features = ["v4", "serde"] }
|
||||
|
||||
@@ -43,5 +45,5 @@ uuid = { version = "0.7", features = ["v4", "serde"] }
|
||||
tempfile = "3.0"
|
||||
walkdir = "2.1"
|
||||
lazy_static = "1.2"
|
||||
pretty_assertions = "0.5.1"
|
||||
pretty_assertions = "0.6.1"
|
||||
paste = "0.1"
|
||||
43
server/assets/index.css
Normal file
43
server/assets/index.css
Normal file
@@ -0,0 +1,43 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 60rem;
|
||||
background-color: #efefef;
|
||||
border: 1px solid #666;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.docs {
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Rojo</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 60rem;
|
||||
background-color: #efefef;
|
||||
border: 1px solid #666;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.docs {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="main">
|
||||
<h1 class="title">Rojo Live Sync is up and running!</h1>
|
||||
<a class="docs" href="https://lpghatguy.github.io/rojo">Rojo Documentation</a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
66
server/assets/place.project.json
Normal file
66
server/assets/place.project.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "[placeholder]",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"HttpService": {
|
||||
"$className": "HttpService",
|
||||
"$properties": {
|
||||
"HttpEnabled": true
|
||||
}
|
||||
},
|
||||
"Lighting": {
|
||||
"$className": "Lighting",
|
||||
"$properties": {
|
||||
"Ambient": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"Brightness": 2,
|
||||
"GlobalShadows": true,
|
||||
"Outlines": false,
|
||||
"Technology": "Voxel"
|
||||
}
|
||||
},
|
||||
"ReplicatedStorage": {
|
||||
"$className": "ReplicatedStorage",
|
||||
"Source": {
|
||||
"$path": "src"
|
||||
}
|
||||
},
|
||||
"SoundService": {
|
||||
"$className": "SoundService",
|
||||
"$properties": {
|
||||
"RespectFilteringEnabled": true
|
||||
}
|
||||
},
|
||||
"Workspace": {
|
||||
"$className": "Workspace",
|
||||
"$properties": {
|
||||
"FilteringEnabled": true
|
||||
},
|
||||
"Baseplate": {
|
||||
"$className": "Part",
|
||||
"$properties": {
|
||||
"Anchored": true,
|
||||
"Color": [
|
||||
0.38823,
|
||||
0.37254,
|
||||
0.38823
|
||||
],
|
||||
"Locked": true,
|
||||
"Position": [
|
||||
0,
|
||||
-10,
|
||||
0
|
||||
],
|
||||
"Size": [
|
||||
512,
|
||||
20,
|
||||
512
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
fs::File,
|
||||
io,
|
||||
io::{self, Write, BufWriter},
|
||||
};
|
||||
|
||||
use log::info;
|
||||
use failure::Fail;
|
||||
|
||||
use crate::{
|
||||
rbx_session::construct_oneoff_tree,
|
||||
project::{Project, ProjectLoadFuzzyError},
|
||||
imfs::{Imfs, FsError},
|
||||
project::{Project, ProjectLoadFuzzyError},
|
||||
rbx_session::construct_oneoff_tree,
|
||||
rbx_snapshot::SnapshotError,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -59,6 +60,9 @@ pub enum BuildError {
|
||||
|
||||
#[fail(display = "{}", _0)]
|
||||
FsError(#[fail(cause)] FsError),
|
||||
|
||||
#[fail(display = "{}", _0)]
|
||||
SnapshotError(#[fail(cause)] SnapshotError),
|
||||
}
|
||||
|
||||
impl_from!(BuildError {
|
||||
@@ -67,8 +71,14 @@ impl_from!(BuildError {
|
||||
rbx_xml::EncodeError => XmlModelEncodeError,
|
||||
rbx_binary::EncodeError => BinaryModelEncodeError,
|
||||
FsError => FsError,
|
||||
SnapshotError => SnapshotError,
|
||||
});
|
||||
|
||||
fn xml_encode_config() -> rbx_xml::EncodeOptions {
|
||||
rbx_xml::EncodeOptions::new()
|
||||
.property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
|
||||
}
|
||||
|
||||
pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
|
||||
let output_kind = options.output_kind
|
||||
.or_else(|| detect_output_kind(options))
|
||||
@@ -86,8 +96,8 @@ pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
|
||||
|
||||
let mut imfs = Imfs::new();
|
||||
imfs.add_roots_from_project(&project)?;
|
||||
let tree = construct_oneoff_tree(&project, &imfs);
|
||||
let mut file = File::create(&options.output_file)?;
|
||||
let tree = construct_oneoff_tree(&project, &imfs)?;
|
||||
let mut file = BufWriter::new(File::create(&options.output_file)?);
|
||||
|
||||
match output_kind {
|
||||
OutputKind::Rbxmx => {
|
||||
@@ -95,7 +105,7 @@ pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
|
||||
// descendants.
|
||||
|
||||
let root_id = tree.get_root_id();
|
||||
rbx_xml::encode(&tree, &[root_id], &mut file)?;
|
||||
rbx_xml::to_writer(&mut file, &tree, &[root_id], xml_encode_config())?;
|
||||
},
|
||||
OutputKind::Rbxlx => {
|
||||
// Place files don't contain an entry for the DataModel, but our
|
||||
@@ -103,7 +113,7 @@ pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
|
||||
|
||||
let root_id = tree.get_root_id();
|
||||
let top_level_ids = tree.get_instance(root_id).unwrap().get_children_ids();
|
||||
rbx_xml::encode(&tree, top_level_ids, &mut file)?;
|
||||
rbx_xml::to_writer(&mut file, &tree, top_level_ids, xml_encode_config())?;
|
||||
},
|
||||
OutputKind::Rbxm => {
|
||||
let root_id = tree.get_root_id();
|
||||
@@ -116,5 +126,7 @@ pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
|
||||
},
|
||||
}
|
||||
|
||||
file.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -8,9 +8,9 @@ use failure::Fail;
|
||||
|
||||
use crate::{
|
||||
project::{Project, ProjectLoadFuzzyError},
|
||||
web::Server,
|
||||
web::LiveServer,
|
||||
imfs::FsError,
|
||||
live_session::LiveSession,
|
||||
live_session::{LiveSession, LiveSessionError},
|
||||
};
|
||||
|
||||
const DEFAULT_PORT: u16 = 34872;
|
||||
@@ -28,11 +28,15 @@ pub enum ServeError {
|
||||
|
||||
#[fail(display = "{}", _0)]
|
||||
FsError(#[fail(cause)] FsError),
|
||||
|
||||
#[fail(display = "{}", _0)]
|
||||
LiveSessionError(#[fail(cause)] LiveSessionError),
|
||||
}
|
||||
|
||||
impl_from!(ServeError {
|
||||
ProjectLoadFuzzyError => ProjectLoadError,
|
||||
FsError => FsError,
|
||||
LiveSessionError => LiveSessionError,
|
||||
});
|
||||
|
||||
pub fn serve(options: &ServeOptions) -> Result<(), ServeError> {
|
||||
@@ -45,7 +49,7 @@ pub fn serve(options: &ServeOptions) -> Result<(), ServeError> {
|
||||
info!("Using project {:#?}", project);
|
||||
|
||||
let live_session = Arc::new(LiveSession::new(Arc::clone(&project))?);
|
||||
let server = Server::new(Arc::clone(&live_session));
|
||||
let server = LiveServer::new(live_session);
|
||||
|
||||
let port = options.port
|
||||
.or(project.serve_port)
|
||||
@@ -53,7 +57,7 @@ pub fn serve(options: &ServeOptions) -> Result<(), ServeError> {
|
||||
|
||||
println!("Rojo server listening on port {}", port);
|
||||
|
||||
server.listen(port);
|
||||
server.start(port);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -9,9 +9,10 @@ use failure::Fail;
|
||||
use reqwest::header::{ACCEPT, USER_AGENT, CONTENT_TYPE, COOKIE};
|
||||
|
||||
use crate::{
|
||||
rbx_session::construct_oneoff_tree,
|
||||
project::{Project, ProjectLoadFuzzyError},
|
||||
imfs::{Imfs, FsError},
|
||||
project::{Project, ProjectLoadFuzzyError},
|
||||
rbx_session::construct_oneoff_tree,
|
||||
rbx_snapshot::SnapshotError,
|
||||
};
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
@@ -36,6 +37,9 @@ pub enum UploadError {
|
||||
|
||||
#[fail(display = "{}", _0)]
|
||||
FsError(#[fail(cause)] FsError),
|
||||
|
||||
#[fail(display = "{}", _0)]
|
||||
SnapshotError(#[fail(cause)] SnapshotError),
|
||||
}
|
||||
|
||||
impl_from!(UploadError {
|
||||
@@ -44,6 +48,7 @@ impl_from!(UploadError {
|
||||
reqwest::Error => HttpError,
|
||||
rbx_xml::EncodeError => XmlModelEncodeError,
|
||||
FsError => FsError,
|
||||
SnapshotError => SnapshotError,
|
||||
});
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -67,7 +72,7 @@ pub fn upload(options: &UploadOptions) -> Result<(), UploadError> {
|
||||
|
||||
let mut imfs = Imfs::new();
|
||||
imfs.add_roots_from_project(&project)?;
|
||||
let tree = construct_oneoff_tree(&project, &imfs);
|
||||
let tree = construct_oneoff_tree(&project, &imfs)?;
|
||||
|
||||
let root_id = tree.get_root_id();
|
||||
let mut contents = Vec::new();
|
||||
@@ -75,10 +80,10 @@ pub fn upload(options: &UploadOptions) -> Result<(), UploadError> {
|
||||
match options.kind {
|
||||
Some("place") | None => {
|
||||
let top_level_ids = tree.get_instance(root_id).unwrap().get_children_ids();
|
||||
rbx_xml::encode(&tree, top_level_ids, &mut contents)?;
|
||||
rbx_xml::to_writer_default(&mut contents, &tree, top_level_ids)?;
|
||||
},
|
||||
Some("model") => {
|
||||
rbx_xml::encode(&tree, &[root_id], &mut contents)?;
|
||||
rbx_xml::to_writer_default(&mut contents, &tree, &[root_id])?;
|
||||
},
|
||||
Some(invalid) => return Err(UploadError::InvalidKind(invalid.to_owned())),
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::{self, Path, PathBuf},
|
||||
cmp::Ordering,
|
||||
collections::{HashMap, HashSet, BTreeSet},
|
||||
fmt,
|
||||
fs,
|
||||
io,
|
||||
path::{self, Path, PathBuf},
|
||||
};
|
||||
|
||||
use failure::Fail;
|
||||
use serde_derive::{Serialize, Deserialize};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use crate::project::{Project, ProjectNode};
|
||||
|
||||
@@ -88,11 +89,13 @@ impl Imfs {
|
||||
|
||||
pub fn add_root(&mut self, path: &Path) -> Result<(), FsError> {
|
||||
debug_assert!(path.is_absolute());
|
||||
debug_assert!(!self.is_within_roots(path));
|
||||
|
||||
self.roots.insert(path.to_path_buf());
|
||||
if !self.is_within_roots(path) {
|
||||
self.roots.insert(path.to_path_buf());
|
||||
self.descend_and_read_from_disk(path)?;
|
||||
}
|
||||
|
||||
self.descend_and_read_from_disk(path)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_root(&mut self, path: &Path) {
|
||||
@@ -237,7 +240,7 @@ impl Imfs {
|
||||
} else if metadata.is_dir() {
|
||||
let item = ImfsItem::Directory(ImfsDirectory {
|
||||
path: path.to_path_buf(),
|
||||
children: HashSet::new(),
|
||||
children: BTreeSet::new(),
|
||||
});
|
||||
|
||||
self.items.insert(path.to_path_buf(), item);
|
||||
@@ -285,19 +288,43 @@ impl Imfs {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ImfsFile {
|
||||
pub path: PathBuf,
|
||||
pub contents: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ImfsDirectory {
|
||||
pub path: PathBuf,
|
||||
pub children: HashSet<PathBuf>,
|
||||
impl PartialOrd for ImfsFile {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
impl Ord for ImfsFile {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.path.cmp(&other.path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ImfsDirectory {
|
||||
pub path: PathBuf,
|
||||
pub children: BTreeSet<PathBuf>,
|
||||
}
|
||||
|
||||
impl PartialOrd for ImfsDirectory {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for ImfsDirectory {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.path.cmp(&other.path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum ImfsItem {
|
||||
File(ImfsFile),
|
||||
Directory(ImfsDirectory),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![recursion_limit="128"]
|
||||
|
||||
// Macros
|
||||
#[macro_use]
|
||||
pub mod impl_from;
|
||||
@@ -16,5 +18,4 @@ pub mod rbx_snapshot;
|
||||
pub mod session_id;
|
||||
pub mod snapshot_reconciler;
|
||||
pub mod visualize;
|
||||
pub mod web;
|
||||
pub mod web_util;
|
||||
pub mod web;
|
||||
@@ -1,21 +1,40 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
mem,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use failure::Fail;
|
||||
|
||||
use crate::{
|
||||
fs_watcher::FsWatcher,
|
||||
imfs::{Imfs, FsError},
|
||||
message_queue::MessageQueue,
|
||||
project::Project,
|
||||
rbx_session::RbxSession,
|
||||
rbx_snapshot::SnapshotError,
|
||||
session_id::SessionId,
|
||||
snapshot_reconciler::InstanceChanges,
|
||||
};
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum LiveSessionError {
|
||||
#[fail(display = "{}", _0)]
|
||||
Fs(#[fail(cause)] FsError),
|
||||
|
||||
#[fail(display = "{}", _0)]
|
||||
Snapshot(#[fail(cause)] SnapshotError),
|
||||
}
|
||||
|
||||
impl_from!(LiveSessionError {
|
||||
FsError => Fs,
|
||||
SnapshotError => Snapshot,
|
||||
});
|
||||
|
||||
/// Contains all of the state for a Rojo live-sync session.
|
||||
pub struct LiveSession {
|
||||
pub project: Arc<Project>,
|
||||
pub session_id: SessionId,
|
||||
project: Arc<Project>,
|
||||
session_id: SessionId,
|
||||
pub message_queue: Arc<MessageQueue<InstanceChanges>>,
|
||||
pub rbx_session: Arc<Mutex<RbxSession>>,
|
||||
pub imfs: Arc<Mutex<Imfs>>,
|
||||
@@ -23,7 +42,7 @@ pub struct LiveSession {
|
||||
}
|
||||
|
||||
impl LiveSession {
|
||||
pub fn new(project: Arc<Project>) -> Result<LiveSession, FsError> {
|
||||
pub fn new(project: Arc<Project>) -> Result<LiveSession, LiveSessionError> {
|
||||
let imfs = {
|
||||
let mut imfs = Imfs::new();
|
||||
imfs.add_roots_from_project(&project)?;
|
||||
@@ -36,7 +55,7 @@ impl LiveSession {
|
||||
Arc::clone(&project),
|
||||
Arc::clone(&imfs),
|
||||
Arc::clone(&message_queue),
|
||||
)));
|
||||
)?));
|
||||
|
||||
let fs_watcher = FsWatcher::start(
|
||||
Arc::clone(&imfs),
|
||||
@@ -46,8 +65,8 @@ impl LiveSession {
|
||||
let session_id = SessionId::new();
|
||||
|
||||
Ok(LiveSession {
|
||||
project,
|
||||
session_id,
|
||||
project,
|
||||
message_queue,
|
||||
rbx_session,
|
||||
imfs,
|
||||
@@ -55,7 +74,26 @@ impl LiveSession {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_project(&self) -> &Project {
|
||||
/// Restarts the live session using the given project while preserving the
|
||||
/// internal session ID.
|
||||
pub fn restart_with_new_project(&mut self, project: Arc<Project>) -> Result<(), LiveSessionError> {
|
||||
let mut new_session = LiveSession::new(project)?;
|
||||
new_session.session_id = self.session_id;
|
||||
|
||||
mem::replace(self, new_session);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn root_project(&self) -> &Project {
|
||||
&self.project
|
||||
}
|
||||
|
||||
pub fn session_id(&self) -> SessionId {
|
||||
self.session_id
|
||||
}
|
||||
|
||||
pub fn serve_place_ids(&self) -> &Option<HashSet<u64>> {
|
||||
&self.project.serve_place_ids
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,83 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
mem,
|
||||
sync::{
|
||||
mpsc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
RwLock,
|
||||
Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
/// A unique identifier, not guaranteed to be generated in any order.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ListenerId(usize);
|
||||
use futures::sync::oneshot;
|
||||
|
||||
/// Generate a new ID, which has no defined ordering.
|
||||
pub fn get_listener_id() -> ListenerId {
|
||||
static LAST_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
struct Listener<T> {
|
||||
sender: oneshot::Sender<(u32, Vec<T>)>,
|
||||
cursor: u32,
|
||||
}
|
||||
|
||||
ListenerId(LAST_ID.fetch_add(1, Ordering::SeqCst))
|
||||
fn fire_listener_if_ready<T: Clone>(messages: &[T], listener: Listener<T>) -> Result<(), Listener<T>> {
|
||||
let current_cursor = messages.len() as u32;
|
||||
|
||||
if listener.cursor < current_cursor {
|
||||
let new_messages = messages[(listener.cursor as usize)..].to_vec();
|
||||
let _ = listener.sender.send((current_cursor, new_messages));
|
||||
Ok(())
|
||||
} else {
|
||||
Err(listener)
|
||||
}
|
||||
}
|
||||
|
||||
/// A message queue with persistent history that can be subscribed to.
|
||||
///
|
||||
/// Definitely non-optimal, but a simple design that works well for the
|
||||
/// synchronous web server Rojo uses, Rouille.
|
||||
/// Definitely non-optimal. This would ideally be a lockless mpmc queue.
|
||||
#[derive(Default)]
|
||||
pub struct MessageQueue<T> {
|
||||
messages: RwLock<Vec<T>>,
|
||||
message_listeners: Mutex<HashMap<ListenerId, mpsc::Sender<()>>>,
|
||||
message_listeners: Mutex<Vec<Listener<T>>>,
|
||||
}
|
||||
|
||||
impl<T: Clone> MessageQueue<T> {
|
||||
pub fn new() -> MessageQueue<T> {
|
||||
MessageQueue {
|
||||
messages: RwLock::new(Vec::new()),
|
||||
message_listeners: Mutex::new(HashMap::new()),
|
||||
message_listeners: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_messages(&self, new_messages: &[T]) {
|
||||
let message_listeners = self.message_listeners.lock().unwrap();
|
||||
let mut message_listeners = self.message_listeners.lock().unwrap();
|
||||
let mut messages = self.messages.write().unwrap();
|
||||
messages.extend_from_slice(new_messages);
|
||||
|
||||
{
|
||||
let mut messages = self.messages.write().unwrap();
|
||||
messages.extend_from_slice(new_messages);
|
||||
let mut remaining_listeners = Vec::new();
|
||||
|
||||
for listener in message_listeners.drain(..) {
|
||||
match fire_listener_if_ready(&messages, listener) {
|
||||
Ok(_) => {}
|
||||
Err(listener) => remaining_listeners.push(listener)
|
||||
}
|
||||
}
|
||||
|
||||
for listener in message_listeners.values() {
|
||||
listener.send(()).unwrap();
|
||||
}
|
||||
// Without this annotation, Rust gets confused since the first argument
|
||||
// is a MutexGuard, but the second is a Vec.
|
||||
mem::replace::<Vec<_>>(&mut message_listeners, remaining_listeners);
|
||||
}
|
||||
|
||||
pub fn subscribe(&self, sender: mpsc::Sender<()>) -> ListenerId {
|
||||
let id = get_listener_id();
|
||||
pub fn subscribe(&self, cursor: u32, sender: oneshot::Sender<(u32, Vec<T>)>) {
|
||||
let listener = {
|
||||
let listener = Listener {
|
||||
sender,
|
||||
cursor,
|
||||
};
|
||||
|
||||
let messages = self.messages.read().unwrap();
|
||||
|
||||
match fire_listener_if_ready(&messages, listener) {
|
||||
Ok(_) => return,
|
||||
Err(listener) => listener
|
||||
}
|
||||
};
|
||||
|
||||
let mut message_listeners = self.message_listeners.lock().unwrap();
|
||||
message_listeners.insert(id, sender);
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
pub fn unsubscribe(&self, id: ListenerId) {
|
||||
let mut message_listeners = self.message_listeners.lock().unwrap();
|
||||
message_listeners.remove(&id);
|
||||
message_listeners.push(listener);
|
||||
}
|
||||
|
||||
pub fn get_message_cursor(&self) -> u32 {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
};
|
||||
|
||||
use serde_derive::Serialize;
|
||||
use serde::Serialize;
|
||||
use log::warn;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -20,6 +20,12 @@ pub struct PathMap<T> {
|
||||
nodes: HashMap<PathBuf, PathMapNode<T>>,
|
||||
}
|
||||
|
||||
impl<T> Default for PathMap<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PathMap<T> {
|
||||
pub fn new() -> PathMap<T> {
|
||||
PathMap {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
//!
|
||||
//! ```
|
||||
//! # use std::path::PathBuf;
|
||||
//! # use serde_derive::{Serialize, Deserialize};
|
||||
//! # use serde::{Serialize, Deserialize};
|
||||
//!
|
||||
//! #[derive(Serialize, Deserialize)]
|
||||
//! struct Mine {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
collections::{HashMap, HashSet, BTreeMap},
|
||||
fmt,
|
||||
fs::{self, File},
|
||||
io,
|
||||
@@ -8,9 +8,10 @@ use std::{
|
||||
|
||||
use log::warn;
|
||||
use failure::Fail;
|
||||
use maplit::hashmap;
|
||||
use rbx_tree::RbxValue;
|
||||
use serde_derive::{Serialize, Deserialize};
|
||||
use rbx_dom_weak::{UnresolvedRbxValue, RbxValue};
|
||||
use serde::{Serialize, Serializer, Deserialize};
|
||||
|
||||
static DEFAULT_PLACE: &'static str = include_str!("../assets/place.project.json");
|
||||
|
||||
pub static PROJECT_FILENAME: &'static str = "default.project.json";
|
||||
pub static COMPAT_PROJECT_FILENAME: &'static str = "roblox-project.json";
|
||||
@@ -24,6 +25,10 @@ struct SourceProject {
|
||||
name: String,
|
||||
tree: SourceProjectNode,
|
||||
|
||||
#[cfg_attr(not(feature = "plugins-enabled"), serde(skip_deserializing))]
|
||||
#[serde(default = "Vec::new", skip_serializing_if = "Vec::is_empty")]
|
||||
plugins: Vec<SourcePlugin>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
serve_port: Option<u16>,
|
||||
|
||||
@@ -33,12 +38,17 @@ struct SourceProject {
|
||||
|
||||
impl SourceProject {
|
||||
/// Consumes the SourceProject and yields a Project, ready for prime-time.
|
||||
pub fn into_project(self, project_file_location: &Path) -> Project {
|
||||
pub fn into_project(mut self, project_file_location: &Path) -> Project {
|
||||
let tree = self.tree.into_project_node(project_file_location);
|
||||
let plugins = self.plugins
|
||||
.drain(..)
|
||||
.map(|source_plugin| source_plugin.into_plugin(project_file_location))
|
||||
.collect();
|
||||
|
||||
Project {
|
||||
name: self.name,
|
||||
tree,
|
||||
plugins,
|
||||
serve_port: self.serve_port,
|
||||
serve_place_ids: self.serve_place_ids,
|
||||
file_location: PathBuf::from(project_file_location),
|
||||
@@ -46,16 +56,89 @@ impl SourceProject {
|
||||
}
|
||||
}
|
||||
|
||||
/// An alternative serializer for `UnresolvedRbxValue` that uses the minimum
|
||||
/// representation of the value.
|
||||
///
|
||||
/// For example, the default Serialize impl might give you:
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "Type": "Bool",
|
||||
/// "Value": true
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// But in reality, users are expected to write just:
|
||||
///
|
||||
/// ```json
|
||||
/// true
|
||||
/// ```
|
||||
///
|
||||
/// This holds true for other values that might be ambiguous or just have more
|
||||
/// complicated representations like enums.
|
||||
fn serialize_unresolved_minimal<S>(unresolved: &UnresolvedRbxValue, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer
|
||||
{
|
||||
match unresolved {
|
||||
UnresolvedRbxValue::Ambiguous(_) => unresolved.serialize(serializer),
|
||||
UnresolvedRbxValue::Concrete(concrete) => {
|
||||
match concrete {
|
||||
RbxValue::Bool { value } => value.serialize(serializer),
|
||||
RbxValue::CFrame { value } => value.serialize(serializer),
|
||||
RbxValue::Color3 { value } => value.serialize(serializer),
|
||||
RbxValue::Color3uint8 { value } => value.serialize(serializer),
|
||||
RbxValue::Content { value } => value.serialize(serializer),
|
||||
RbxValue::Enum { value } => value.serialize(serializer),
|
||||
RbxValue::Float32 { value } => value.serialize(serializer),
|
||||
RbxValue::Int32 { value } => value.serialize(serializer),
|
||||
RbxValue::String { value } => value.serialize(serializer),
|
||||
RbxValue::UDim { value } => value.serialize(serializer),
|
||||
RbxValue::UDim2 { value } => value.serialize(serializer),
|
||||
RbxValue::Vector2 { value } => value.serialize(serializer),
|
||||
RbxValue::Vector2int16 { value } => value.serialize(serializer),
|
||||
RbxValue::Vector3 { value } => value.serialize(serializer),
|
||||
RbxValue::Vector3int16 { value } => value.serialize(serializer),
|
||||
_ => concrete.serialize(serializer),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around serialize_unresolved_minimal that handles the HashMap case.
|
||||
fn serialize_unresolved_map<S>(value: &HashMap<String, UnresolvedRbxValue>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer
|
||||
{
|
||||
use serde::ser::SerializeMap;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Minimal<'a>(
|
||||
#[serde(serialize_with = "serialize_unresolved_minimal")]
|
||||
&'a UnresolvedRbxValue
|
||||
);
|
||||
|
||||
let mut map = serializer.serialize_map(Some(value.len()))?;
|
||||
for (k, v) in value {
|
||||
map.serialize_key(k)?;
|
||||
map.serialize_value(&Minimal(v))?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
|
||||
/// Similar to SourceProject, the structure of nodes in the project tree is
|
||||
/// slightly different on-disk than how we want to handle them in the rest of
|
||||
/// Rojo.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct SourceProjectNode {
|
||||
#[serde(rename = "$className", skip_serializing_if = "Option::is_none")]
|
||||
class_name: Option<String>,
|
||||
|
||||
#[serde(rename = "$properties", default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
|
||||
properties: HashMap<String, RbxValue>,
|
||||
#[serde(
|
||||
rename = "$properties",
|
||||
default = "HashMap::new",
|
||||
skip_serializing_if = "HashMap::is_empty",
|
||||
serialize_with = "serialize_unresolved_map",
|
||||
)]
|
||||
properties: HashMap<String, UnresolvedRbxValue>,
|
||||
|
||||
#[serde(rename = "$ignoreUnknownInstances", skip_serializing_if = "Option::is_none")]
|
||||
ignore_unknown_instances: Option<bool>,
|
||||
@@ -64,14 +147,14 @@ struct SourceProjectNode {
|
||||
path: Option<String>,
|
||||
|
||||
#[serde(flatten)]
|
||||
children: HashMap<String, SourceProjectNode>,
|
||||
children: BTreeMap<String, SourceProjectNode>,
|
||||
}
|
||||
|
||||
impl SourceProjectNode {
|
||||
/// Consumes the SourceProjectNode and turns it into a ProjectNode.
|
||||
pub fn into_project_node(mut self, project_file_location: &Path) -> ProjectNode {
|
||||
let children = self.children.drain()
|
||||
.map(|(key, value)| (key, value.into_project_node(project_file_location)))
|
||||
pub fn into_project_node(self, project_file_location: &Path) -> ProjectNode {
|
||||
let children = self.children.iter()
|
||||
.map(|(key, value)| (key.clone(), value.clone().into_project_node(project_file_location)))
|
||||
.collect();
|
||||
|
||||
// Make sure that paths are absolute, transforming them by adding the
|
||||
@@ -95,6 +178,26 @@ impl SourceProjectNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct SourcePlugin {
|
||||
path: String,
|
||||
}
|
||||
|
||||
impl SourcePlugin {
|
||||
pub fn into_plugin(self, project_file_location: &Path) -> Plugin {
|
||||
let path = if Path::new(&self.path).is_absolute() {
|
||||
PathBuf::from(self.path)
|
||||
} else {
|
||||
let project_folder_location = project_file_location.parent().unwrap();
|
||||
project_folder_location.join(self.path)
|
||||
};
|
||||
|
||||
Plugin {
|
||||
path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned by Project::load_exact
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum ProjectLoadExactError {
|
||||
@@ -133,6 +236,7 @@ pub enum ProjectInitError {
|
||||
AlreadyExists(PathBuf),
|
||||
IoError(#[fail(cause)] io::Error),
|
||||
SaveError(#[fail(cause)] ProjectSaveError),
|
||||
JsonError(#[fail(cause)] serde_json::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for ProjectInitError {
|
||||
@@ -141,6 +245,7 @@ impl fmt::Display for ProjectInitError {
|
||||
ProjectInitError::AlreadyExists(path) => write!(output, "Path {} already exists", path.display()),
|
||||
ProjectInitError::IoError(inner) => write!(output, "IO error: {}", inner),
|
||||
ProjectInitError::SaveError(inner) => write!(output, "{}", inner),
|
||||
ProjectInitError::JsonError(inner) => write!(output, "{}", inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,8 +263,8 @@ pub enum ProjectSaveError {
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct ProjectNode {
|
||||
pub class_name: Option<String>,
|
||||
pub children: HashMap<String, ProjectNode>,
|
||||
pub properties: HashMap<String, RbxValue>,
|
||||
pub children: BTreeMap<String, ProjectNode>,
|
||||
pub properties: HashMap<String, UnresolvedRbxValue>,
|
||||
pub ignore_unknown_instances: Option<bool>,
|
||||
|
||||
#[serde(serialize_with = "crate::path_serializer::serialize_option")]
|
||||
@@ -198,10 +303,30 @@ impl ProjectNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Plugin {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
fn to_source_plugin(&self, project_file_location: &Path) -> SourcePlugin {
|
||||
let project_folder_location = project_file_location.parent().unwrap();
|
||||
let path = match self.path.strip_prefix(project_folder_location) {
|
||||
Ok(stripped) => stripped.to_str().unwrap().replace("\\", "/"),
|
||||
Err(_) => format!("{}", self.path.display()),
|
||||
};
|
||||
|
||||
SourcePlugin {
|
||||
path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Project {
|
||||
pub name: String,
|
||||
pub tree: ProjectNode,
|
||||
pub plugins: Vec<Plugin>,
|
||||
pub serve_port: Option<u16>,
|
||||
pub serve_place_ids: Option<HashSet<u64>>,
|
||||
pub file_location: PathBuf,
|
||||
@@ -210,46 +335,16 @@ pub struct Project {
|
||||
impl Project {
|
||||
pub fn init_place(project_fuzzy_path: &Path) -> Result<PathBuf, ProjectInitError> {
|
||||
let project_path = Project::init_pick_path(project_fuzzy_path)?;
|
||||
let project_folder_path = project_path.parent().unwrap();
|
||||
let project_name = if project_fuzzy_path == project_path {
|
||||
project_fuzzy_path.parent().unwrap().file_name().unwrap().to_str().unwrap()
|
||||
} else {
|
||||
project_fuzzy_path.file_name().unwrap().to_str().unwrap()
|
||||
};
|
||||
|
||||
let tree = ProjectNode {
|
||||
class_name: Some(String::from("DataModel")),
|
||||
children: hashmap! {
|
||||
String::from("ReplicatedStorage") => ProjectNode {
|
||||
class_name: Some(String::from("ReplicatedStorage")),
|
||||
children: hashmap! {
|
||||
String::from("Source") => ProjectNode {
|
||||
path: Some(project_folder_path.join("src")),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
String::from("HttpService") => ProjectNode {
|
||||
class_name: Some(String::from("HttpService")),
|
||||
properties: hashmap! {
|
||||
String::from("HttpEnabled") => RbxValue::Bool {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let mut project = Project::load_from_str(DEFAULT_PLACE, &project_path)
|
||||
.map_err(ProjectInitError::JsonError)?;
|
||||
|
||||
let project = Project {
|
||||
name: project_name.to_string(),
|
||||
tree,
|
||||
serve_port: None,
|
||||
serve_place_ids: None,
|
||||
file_location: project_path.clone(),
|
||||
};
|
||||
project.name = project_name.to_owned();
|
||||
|
||||
project.save()
|
||||
.map_err(ProjectInitError::SaveError)?;
|
||||
@@ -274,6 +369,7 @@ impl Project {
|
||||
let project = Project {
|
||||
name: project_name.to_string(),
|
||||
tree,
|
||||
plugins: Vec::new(),
|
||||
serve_port: None,
|
||||
serve_place_ids: None,
|
||||
file_location: project_path.clone(),
|
||||
@@ -336,6 +432,12 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_from_str(contents: &str, project_file_location: &Path) -> Result<Project, serde_json::Error> {
|
||||
let parsed: SourceProject = serde_json::from_str(&contents)?;
|
||||
|
||||
Ok(parsed.into_project(project_file_location))
|
||||
}
|
||||
|
||||
pub fn load_fuzzy(fuzzy_project_location: &Path) -> Result<Project, ProjectLoadFuzzyError> {
|
||||
let project_path = Self::locate(fuzzy_project_location)
|
||||
.ok_or(ProjectLoadFuzzyError::NotFound)?;
|
||||
@@ -383,10 +485,20 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn folder_location(&self) -> &Path {
|
||||
self.file_location.parent().unwrap()
|
||||
}
|
||||
|
||||
fn to_source_project(&self) -> SourceProject {
|
||||
let plugins = self.plugins
|
||||
.iter()
|
||||
.map(|plugin| plugin.to_source_plugin(&self.file_location))
|
||||
.collect();
|
||||
|
||||
SourceProject {
|
||||
name: self.name.clone(),
|
||||
tree: self.tree.to_source_node(&self.file_location),
|
||||
plugins,
|
||||
serve_port: self.serve_port,
|
||||
serve_place_ids: self.serve_place_ids.clone(),
|
||||
}
|
||||
|
||||
@@ -6,16 +6,25 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use serde_derive::{Serialize, Deserialize};
|
||||
use log::{info, trace};
|
||||
use rbx_tree::{RbxTree, RbxId};
|
||||
use rlua::Lua;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use log::{info, trace, error};
|
||||
use rbx_dom_weak::{RbxTree, RbxId};
|
||||
|
||||
use crate::{
|
||||
project::{Project, ProjectNode},
|
||||
message_queue::MessageQueue,
|
||||
imfs::{Imfs, ImfsItem},
|
||||
path_map::PathMap,
|
||||
rbx_snapshot::{snapshot_project_tree, snapshot_project_node, snapshot_imfs_path},
|
||||
rbx_snapshot::{
|
||||
SnapshotError,
|
||||
SnapshotContext,
|
||||
SnapshotPluginContext,
|
||||
SnapshotPluginEntry,
|
||||
snapshot_project_tree,
|
||||
snapshot_project_node,
|
||||
snapshot_imfs_path,
|
||||
},
|
||||
snapshot_reconciler::{InstanceChanges, reify_root, reconcile_subtree},
|
||||
};
|
||||
|
||||
@@ -58,22 +67,60 @@ impl RbxSession {
|
||||
project: Arc<Project>,
|
||||
imfs: Arc<Mutex<Imfs>>,
|
||||
message_queue: Arc<MessageQueue<InstanceChanges>>,
|
||||
) -> RbxSession {
|
||||
) -> Result<RbxSession, SnapshotError> {
|
||||
let mut instances_per_path = PathMap::new();
|
||||
let mut metadata_per_instance = HashMap::new();
|
||||
|
||||
let tree = {
|
||||
let temp_imfs = imfs.lock().unwrap();
|
||||
reify_initial_tree(&project, &temp_imfs, &mut instances_per_path, &mut metadata_per_instance)
|
||||
let plugin_context = if cfg!(feature = "server-plugins") {
|
||||
let lua = Lua::new();
|
||||
let mut callback_key = None;
|
||||
|
||||
lua.context(|context| {
|
||||
let callback = context.load(r#"
|
||||
return function(snapshot)
|
||||
print("got my snapshot:", snapshot)
|
||||
print("name:", snapshot.name, "class name:", snapshot.className)
|
||||
end"#)
|
||||
.set_name("a cool plugin").unwrap()
|
||||
.call::<(), rlua::Function>(()).unwrap();
|
||||
|
||||
callback_key = Some(context.create_registry_value(callback).unwrap());
|
||||
});
|
||||
|
||||
let plugins = vec![
|
||||
SnapshotPluginEntry {
|
||||
file_name_filter: String::new(),
|
||||
callback: callback_key.unwrap(),
|
||||
}
|
||||
];
|
||||
|
||||
Some(SnapshotPluginContext { lua, plugins })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
RbxSession {
|
||||
let context = SnapshotContext {
|
||||
plugin_context,
|
||||
};
|
||||
|
||||
let tree = {
|
||||
let temp_imfs = imfs.lock().unwrap();
|
||||
reify_initial_tree(
|
||||
&project,
|
||||
&context,
|
||||
&temp_imfs,
|
||||
&mut instances_per_path,
|
||||
&mut metadata_per_instance,
|
||||
)?
|
||||
};
|
||||
|
||||
Ok(RbxSession {
|
||||
tree,
|
||||
instances_per_path,
|
||||
metadata_per_instance,
|
||||
message_queue,
|
||||
imfs,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn path_created_or_updated(&mut self, path: &Path) {
|
||||
@@ -104,27 +151,37 @@ impl RbxSession {
|
||||
.expect("Metadata did not exist for path")
|
||||
.clone();
|
||||
|
||||
let context = SnapshotContext {
|
||||
plugin_context: None,
|
||||
};
|
||||
|
||||
for instance_id in &instances_at_path {
|
||||
let instance_metadata = self.metadata_per_instance.get(&instance_id)
|
||||
.expect("Metadata for instance ID did not exist");
|
||||
|
||||
let maybe_snapshot = match &instance_metadata.project_definition {
|
||||
Some((instance_name, project_node)) => {
|
||||
snapshot_project_node(&imfs, &project_node, Cow::Owned(instance_name.clone()))
|
||||
.unwrap_or_else(|_| panic!("Could not generate instance snapshot for path {}", path_to_snapshot.display()))
|
||||
snapshot_project_node(&context, &imfs, &project_node, Cow::Owned(instance_name.clone()))
|
||||
// .unwrap_or_else(|_| panic!("Could not generate instance snapshot for path {}", path_to_snapshot.display()))
|
||||
},
|
||||
None => {
|
||||
snapshot_imfs_path(&imfs, &path_to_snapshot, None)
|
||||
.unwrap_or_else(|_| panic!("Could not generate instance snapshot for path {}", path_to_snapshot.display()))
|
||||
snapshot_imfs_path(&context, &imfs, &path_to_snapshot, None)
|
||||
// .unwrap_or_else(|_| panic!("Could not generate instance snapshot for path {}", path_to_snapshot.display()))
|
||||
},
|
||||
};
|
||||
|
||||
let snapshot = match maybe_snapshot {
|
||||
Some(snapshot) => snapshot,
|
||||
None => {
|
||||
Ok(Some(snapshot)) => snapshot,
|
||||
Ok(None) => {
|
||||
trace!("Path resulted in no snapshot being generated.");
|
||||
return;
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Rojo couldn't turn one of the project's files into Roblox instances.");
|
||||
error!("Any changes to the file have been ignored.");
|
||||
error!("{}", err);
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
trace!("Snapshot: {:#?}", snapshot);
|
||||
@@ -194,29 +251,39 @@ impl RbxSession {
|
||||
&self.tree
|
||||
}
|
||||
|
||||
pub fn get_all_instance_metadata(&self) -> &HashMap<RbxId, MetadataPerInstance> {
|
||||
&self.metadata_per_instance
|
||||
}
|
||||
|
||||
pub fn get_instance_metadata(&self, id: RbxId) -> Option<&MetadataPerInstance> {
|
||||
self.metadata_per_instance.get(&id)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn construct_oneoff_tree(project: &Project, imfs: &Imfs) -> RbxTree {
|
||||
pub fn construct_oneoff_tree(project: &Project, imfs: &Imfs) -> Result<RbxTree, SnapshotError> {
|
||||
let mut instances_per_path = PathMap::new();
|
||||
let mut metadata_per_instance = HashMap::new();
|
||||
reify_initial_tree(project, imfs, &mut instances_per_path, &mut metadata_per_instance)
|
||||
let context = SnapshotContext {
|
||||
plugin_context: None,
|
||||
};
|
||||
|
||||
reify_initial_tree(project, &context, imfs, &mut instances_per_path, &mut metadata_per_instance)
|
||||
}
|
||||
|
||||
fn reify_initial_tree(
|
||||
project: &Project,
|
||||
context: &SnapshotContext,
|
||||
imfs: &Imfs,
|
||||
instances_per_path: &mut PathMap<HashSet<RbxId>>,
|
||||
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
|
||||
) -> RbxTree {
|
||||
let snapshot = snapshot_project_tree(imfs, project)
|
||||
.expect("Could not snapshot project tree")
|
||||
.expect("Project did not produce any instances");
|
||||
) -> Result<RbxTree, SnapshotError> {
|
||||
let snapshot = match snapshot_project_tree(&context, imfs, project)? {
|
||||
Some(snapshot) => snapshot,
|
||||
None => panic!("Project did not produce any instances"),
|
||||
};
|
||||
|
||||
let mut changes = InstanceChanges::default();
|
||||
let tree = reify_root(&snapshot, instances_per_path, metadata_per_instance, &mut changes);
|
||||
|
||||
tree
|
||||
Ok(tree)
|
||||
}
|
||||
@@ -9,11 +9,13 @@ use std::{
|
||||
str,
|
||||
};
|
||||
|
||||
use rlua::Lua;
|
||||
use failure::Fail;
|
||||
use log::info;
|
||||
use maplit::hashmap;
|
||||
use rbx_tree::{RbxTree, RbxValue, RbxInstanceProperties};
|
||||
use serde_derive::{Serialize, Deserialize};
|
||||
use rbx_dom_weak::{RbxTree, RbxValue, RbxInstanceProperties, UnresolvedRbxValue};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use rbx_reflection::{try_resolve_value, ValueResolveError};
|
||||
|
||||
use crate::{
|
||||
imfs::{
|
||||
@@ -38,6 +40,53 @@ const INIT_MODULE_NAME: &str = "init.lua";
|
||||
const INIT_SERVER_NAME: &str = "init.server.lua";
|
||||
const INIT_CLIENT_NAME: &str = "init.client.lua";
|
||||
|
||||
pub struct SnapshotContext {
|
||||
pub plugin_context: Option<SnapshotPluginContext>,
|
||||
}
|
||||
|
||||
/// Context that's only relevant to generating snapshots if there are plugins
|
||||
/// associated with the project.
|
||||
///
|
||||
/// It's possible that this needs some sort of extra nesting/filtering to
|
||||
/// support nested projects, since their plugins should only apply to
|
||||
/// themselves.
|
||||
pub struct SnapshotPluginContext {
|
||||
pub lua: Lua,
|
||||
pub plugins: Vec<SnapshotPluginEntry>,
|
||||
}
|
||||
|
||||
pub struct SnapshotPluginEntry {
|
||||
/// Simple file name suffix filter to avoid running plugins on every file
|
||||
/// change.
|
||||
pub file_name_filter: String,
|
||||
|
||||
/// A key into the Lua registry created by [`create_registry_value`] that
|
||||
/// refers to a function that can be called to transform a file/instance
|
||||
/// pair according to how the plugin needs to operate.
|
||||
///
|
||||
/// [`create_registry_value`]: https://docs.rs/rlua/0.16.2/rlua/struct.Context.html#method.create_registry_value
|
||||
pub callback: rlua::RegistryKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LuaRbxSnapshot(RbxSnapshotInstance<'static>);
|
||||
|
||||
impl rlua::UserData for LuaRbxSnapshot {
|
||||
fn add_methods<'lua, M: rlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(rlua::MetaMethod::Index, |_context, this, key: String| {
|
||||
match key.as_str() {
|
||||
"name" => Ok(this.0.name.clone().into_owned()),
|
||||
"className" => Ok(this.0.class_name.clone().into_owned()),
|
||||
_ => Err(rlua::Error::RuntimeError(format!("{} is not a valid member of RbxSnapshotInstance", &key))),
|
||||
}
|
||||
});
|
||||
|
||||
methods.add_meta_method(rlua::MetaMethod::ToString, |_context, _this, _args: ()| {
|
||||
Ok("RbxSnapshotInstance")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub type SnapshotResult<'a> = Result<Option<RbxSnapshotInstance<'a>>, SnapshotError>;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
@@ -57,6 +106,7 @@ pub enum SnapshotError {
|
||||
},
|
||||
|
||||
XmlModelDecodeError {
|
||||
#[fail(cause)]
|
||||
inner: rbx_xml::DecodeError,
|
||||
path: PathBuf,
|
||||
},
|
||||
@@ -66,11 +116,30 @@ pub enum SnapshotError {
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
CsvDecodeError {
|
||||
#[fail(cause)]
|
||||
inner: csv::Error,
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
ProjectNodeUnusable,
|
||||
|
||||
ProjectNodeInvalidTransmute {
|
||||
partition_path: PathBuf,
|
||||
},
|
||||
|
||||
PropertyResolveError {
|
||||
#[fail(cause)]
|
||||
inner: ValueResolveError,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ValueResolveError> for SnapshotError {
|
||||
fn from(inner: ValueResolveError) -> SnapshotError {
|
||||
SnapshotError::PropertyResolveError {
|
||||
inner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SnapshotError {
|
||||
@@ -84,11 +153,14 @@ impl fmt::Display for SnapshotError {
|
||||
write!(output, "Malformed .model.json model: {} in path {}", inner, path.display())
|
||||
},
|
||||
SnapshotError::XmlModelDecodeError { inner, path } => {
|
||||
write!(output, "Malformed rbxmx model: {:?} in path {}", inner, path.display())
|
||||
write!(output, "Malformed rbxmx model: {} in path {}", inner, path.display())
|
||||
},
|
||||
SnapshotError::BinaryModelDecodeError { inner, path } => {
|
||||
write!(output, "Malformed rbxm model: {:?} in path {}", inner, path.display())
|
||||
},
|
||||
SnapshotError::CsvDecodeError { inner, path } => {
|
||||
write!(output, "Malformed csv file: {} in path {}", inner, path.display())
|
||||
},
|
||||
SnapshotError::ProjectNodeUnusable => {
|
||||
write!(output, "Rojo project nodes must specify either $path or $className.")
|
||||
},
|
||||
@@ -99,24 +171,27 @@ impl fmt::Display for SnapshotError {
|
||||
writeln!(output, "")?;
|
||||
writeln!(output, "Partition target ($path): {}", partition_path.display())
|
||||
},
|
||||
SnapshotError::PropertyResolveError { inner } => write!(output, "{}", inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot_project_tree<'source>(
|
||||
context: &SnapshotContext,
|
||||
imfs: &'source Imfs,
|
||||
project: &'source Project,
|
||||
) -> SnapshotResult<'source> {
|
||||
snapshot_project_node(imfs, &project.tree, Cow::Borrowed(&project.name))
|
||||
snapshot_project_node(context, imfs, &project.tree, Cow::Borrowed(&project.name))
|
||||
}
|
||||
|
||||
pub fn snapshot_project_node<'source>(
|
||||
context: &SnapshotContext,
|
||||
imfs: &'source Imfs,
|
||||
node: &ProjectNode,
|
||||
instance_name: Cow<'source, str>,
|
||||
) -> SnapshotResult<'source> {
|
||||
let maybe_snapshot = match &node.path {
|
||||
Some(path) => snapshot_imfs_path(imfs, &path, Some(instance_name.clone()))?,
|
||||
Some(path) => snapshot_imfs_path(context, imfs, &path, Some(instance_name.clone()))?,
|
||||
None => match &node.class_name {
|
||||
Some(_class_name) => Some(RbxSnapshotInstance {
|
||||
name: instance_name.clone(),
|
||||
@@ -170,13 +245,14 @@ pub fn snapshot_project_node<'source>(
|
||||
}
|
||||
|
||||
for (child_name, child_project_node) in &node.children {
|
||||
if let Some(child) = snapshot_project_node(imfs, child_project_node, Cow::Owned(child_name.clone()))? {
|
||||
if let Some(child) = snapshot_project_node(context, imfs, child_project_node, Cow::Owned(child_name.clone()))? {
|
||||
snapshot.children.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
for (key, value) in &node.properties {
|
||||
snapshot.properties.insert(key.clone(), value.clone());
|
||||
let resolved_value = try_resolve_value(&snapshot.class_name, key, value)?;
|
||||
snapshot.properties.insert(key.clone(), resolved_value);
|
||||
}
|
||||
|
||||
if let Some(ignore_unknown_instances) = node.ignore_unknown_instances {
|
||||
@@ -189,6 +265,7 @@ pub fn snapshot_project_node<'source>(
|
||||
}
|
||||
|
||||
pub fn snapshot_imfs_path<'source>(
|
||||
context: &SnapshotContext,
|
||||
imfs: &'source Imfs,
|
||||
path: &Path,
|
||||
instance_name: Option<Cow<'source, str>>,
|
||||
@@ -196,23 +273,25 @@ pub fn snapshot_imfs_path<'source>(
|
||||
// If the given path doesn't exist in the in-memory filesystem, we consider
|
||||
// that an error.
|
||||
match imfs.get(path) {
|
||||
Some(imfs_item) => snapshot_imfs_item(imfs, imfs_item, instance_name),
|
||||
Some(imfs_item) => snapshot_imfs_item(context, imfs, imfs_item, instance_name),
|
||||
None => return Err(SnapshotError::DidNotExist(path.to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot_imfs_item<'source>(
|
||||
context: &SnapshotContext,
|
||||
imfs: &'source Imfs,
|
||||
item: &'source ImfsItem,
|
||||
instance_name: Option<Cow<'source, str>>,
|
||||
) -> SnapshotResult<'source> {
|
||||
match item {
|
||||
ImfsItem::File(file) => snapshot_imfs_file(file, instance_name),
|
||||
ImfsItem::Directory(directory) => snapshot_imfs_directory(imfs, directory, instance_name),
|
||||
ImfsItem::File(file) => snapshot_imfs_file(context, file, instance_name),
|
||||
ImfsItem::Directory(directory) => snapshot_imfs_directory(context, imfs, directory, instance_name),
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot_imfs_directory<'source>(
|
||||
context: &SnapshotContext,
|
||||
imfs: &'source Imfs,
|
||||
directory: &'source ImfsDirectory,
|
||||
instance_name: Option<Cow<'source, str>>,
|
||||
@@ -229,11 +308,11 @@ fn snapshot_imfs_directory<'source>(
|
||||
});
|
||||
|
||||
let mut snapshot = if directory.children.contains(&init_path) {
|
||||
snapshot_imfs_path(imfs, &init_path, Some(snapshot_name))?.unwrap()
|
||||
snapshot_imfs_path(context, imfs, &init_path, Some(snapshot_name))?.unwrap()
|
||||
} else if directory.children.contains(&init_server_path) {
|
||||
snapshot_imfs_path(imfs, &init_server_path, Some(snapshot_name))?.unwrap()
|
||||
snapshot_imfs_path(context, imfs, &init_server_path, Some(snapshot_name))?.unwrap()
|
||||
} else if directory.children.contains(&init_client_path) {
|
||||
snapshot_imfs_path(imfs, &init_client_path, Some(snapshot_name))?.unwrap()
|
||||
snapshot_imfs_path(context, imfs, &init_client_path, Some(snapshot_name))?.unwrap()
|
||||
} else {
|
||||
RbxSnapshotInstance {
|
||||
class_name: Cow::Borrowed("Folder"),
|
||||
@@ -262,7 +341,7 @@ fn snapshot_imfs_directory<'source>(
|
||||
// them here.
|
||||
},
|
||||
_ => {
|
||||
if let Some(child) = snapshot_imfs_path(imfs, child_path, None)? {
|
||||
if let Some(child) = snapshot_imfs_path(context, imfs, child_path, None)? {
|
||||
snapshot.children.push(child);
|
||||
}
|
||||
},
|
||||
@@ -273,6 +352,7 @@ fn snapshot_imfs_directory<'source>(
|
||||
}
|
||||
|
||||
fn snapshot_imfs_file<'source>(
|
||||
context: &SnapshotContext,
|
||||
file: &'source ImfsFile,
|
||||
instance_name: Option<Cow<'source, str>>,
|
||||
) -> SnapshotResult<'source> {
|
||||
@@ -308,6 +388,20 @@ fn snapshot_imfs_file<'source>(
|
||||
info!("File generated no snapshot: {}", file.path.display());
|
||||
}
|
||||
|
||||
if let Some(snapshot) = maybe_snapshot.as_ref() {
|
||||
if let Some(plugin_context) = &context.plugin_context {
|
||||
for plugin in &plugin_context.plugins {
|
||||
let owned_snapshot = snapshot.get_owned();
|
||||
let registry_key = &plugin.callback;
|
||||
|
||||
plugin_context.lua.context(move |context| {
|
||||
let callback: rlua::Function = context.registry_value(registry_key).unwrap();
|
||||
callback.call::<_, ()>(LuaRbxSnapshot(owned_snapshot)).unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(maybe_snapshot)
|
||||
}
|
||||
|
||||
@@ -391,16 +485,87 @@ fn snapshot_txt_file<'source>(
|
||||
fn snapshot_csv_file<'source>(
|
||||
file: &'source ImfsFile,
|
||||
) -> SnapshotResult<'source> {
|
||||
/// Struct that holds any valid row from a Roblox CSV translation table.
|
||||
///
|
||||
/// We manually deserialize into this table from CSV, but let JSON handle
|
||||
/// serializing.
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LocalizationEntry<'a> {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
key: Option<&'a str>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
context: Option<&'a str>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
example: Option<&'a str>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
source: Option<&'a str>,
|
||||
|
||||
values: HashMap<&'a str, &'a str>,
|
||||
}
|
||||
|
||||
let instance_name = file.path
|
||||
.file_stem().expect("Could not extract file stem")
|
||||
.to_str().expect("Could not convert path to UTF-8");
|
||||
|
||||
let entries: Vec<LocalizationEntryJson> = csv::Reader::from_reader(file.contents.as_slice())
|
||||
.deserialize()
|
||||
// TODO: Propagate error upward instead of panicking
|
||||
.map(|result| result.expect("Malformed localization table found!"))
|
||||
.map(LocalizationEntryCsv::to_json)
|
||||
.collect();
|
||||
// Normally, we'd be able to let the csv crate construct our struct for us.
|
||||
//
|
||||
// However, because of a limitation with Serde's 'flatten' feature, it's not
|
||||
// possible presently to losslessly collect extra string values while using
|
||||
// csv+Serde.
|
||||
//
|
||||
// https://github.com/BurntSushi/rust-csv/issues/151
|
||||
let mut reader = csv::Reader::from_reader(file.contents.as_slice());
|
||||
|
||||
let headers = reader.headers()
|
||||
.map_err(|inner| SnapshotError::CsvDecodeError {
|
||||
inner,
|
||||
path: file.path.to_path_buf(),
|
||||
})?
|
||||
.clone();
|
||||
|
||||
let mut records = Vec::new();
|
||||
|
||||
for record in reader.into_records() {
|
||||
let record = record
|
||||
.map_err(|inner| SnapshotError::CsvDecodeError {
|
||||
inner,
|
||||
path: file.path.to_path_buf(),
|
||||
})?;
|
||||
|
||||
records.push(record);
|
||||
}
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for record in &records {
|
||||
let mut entry = LocalizationEntry::default();
|
||||
|
||||
for (header, value) in headers.iter().zip(record.into_iter()) {
|
||||
if header.is_empty() || value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match header {
|
||||
"Key" => entry.key = Some(value),
|
||||
"Source" => entry.source = Some(value),
|
||||
"Context" => entry.context = Some(value),
|
||||
"Example" => entry.example = Some(value),
|
||||
_ => {
|
||||
entry.values.insert(header, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if entry.key.is_none() && entry.source.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
let table_contents = serde_json::to_string(&entries)
|
||||
.expect("Could not encode JSON for localization table");
|
||||
@@ -422,39 +587,6 @@ fn snapshot_csv_file<'source>(
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct LocalizationEntryCsv {
|
||||
key: String,
|
||||
context: String,
|
||||
example: String,
|
||||
source: String,
|
||||
#[serde(flatten)]
|
||||
values: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl LocalizationEntryCsv {
|
||||
fn to_json(self) -> LocalizationEntryJson {
|
||||
LocalizationEntryJson {
|
||||
key: self.key,
|
||||
context: self.context,
|
||||
example: self.example,
|
||||
source: self.source,
|
||||
values: self.values,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LocalizationEntryJson {
|
||||
key: String,
|
||||
context: String,
|
||||
example: String,
|
||||
source: String,
|
||||
values: HashMap<String, String>,
|
||||
}
|
||||
|
||||
fn snapshot_json_model_file<'source>(
|
||||
file: &'source ImfsFile,
|
||||
) -> SnapshotResult<'source> {
|
||||
@@ -470,7 +602,7 @@ fn snapshot_json_model_file<'source>(
|
||||
path: file.path.to_owned(),
|
||||
})?;
|
||||
|
||||
let mut snapshot = json_instance.into_snapshot();
|
||||
let mut snapshot = json_instance.into_snapshot()?;
|
||||
snapshot.metadata.source_path = Some(file.path.to_owned());
|
||||
|
||||
Ok(Some(snapshot))
|
||||
@@ -486,23 +618,31 @@ struct JsonModelInstance {
|
||||
children: Vec<JsonModelInstance>,
|
||||
|
||||
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
|
||||
properties: HashMap<String, RbxValue>,
|
||||
properties: HashMap<String, UnresolvedRbxValue>,
|
||||
}
|
||||
|
||||
impl JsonModelInstance {
|
||||
fn into_snapshot(mut self) -> RbxSnapshotInstance<'static> {
|
||||
let children = self.children
|
||||
.drain(..)
|
||||
.map(JsonModelInstance::into_snapshot)
|
||||
.collect();
|
||||
fn into_snapshot(self) -> Result<RbxSnapshotInstance<'static>, SnapshotError> {
|
||||
let mut children = Vec::with_capacity(self.children.len());
|
||||
|
||||
RbxSnapshotInstance {
|
||||
for child in self.children {
|
||||
children.push(child.into_snapshot()?);
|
||||
}
|
||||
|
||||
let mut properties = HashMap::with_capacity(self.properties.len());
|
||||
|
||||
for (key, value) in self.properties {
|
||||
let resolved_value = try_resolve_value(&self.class_name, &key, &value)?;
|
||||
properties.insert(key, resolved_value);
|
||||
}
|
||||
|
||||
Ok(RbxSnapshotInstance {
|
||||
name: Cow::Owned(self.name),
|
||||
class_name: Cow::Owned(self.class_name),
|
||||
properties: self.properties,
|
||||
properties,
|
||||
children,
|
||||
metadata: Default::default(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,20 +653,16 @@ fn snapshot_xml_model_file<'source>(
|
||||
.file_stem().expect("Could not extract file stem")
|
||||
.to_str().expect("Could not convert path to UTF-8");
|
||||
|
||||
let mut temp_tree = RbxTree::new(RbxInstanceProperties {
|
||||
name: "Temp".to_owned(),
|
||||
class_name: "Folder".to_owned(),
|
||||
properties: HashMap::new(),
|
||||
});
|
||||
let options = rbx_xml::DecodeOptions::new()
|
||||
.property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown);
|
||||
|
||||
let root_id = temp_tree.get_root_id();
|
||||
rbx_xml::decode(&mut temp_tree, root_id, file.contents.as_slice())
|
||||
let temp_tree = rbx_xml::from_reader(file.contents.as_slice(), options)
|
||||
.map_err(|inner| SnapshotError::XmlModelDecodeError {
|
||||
inner,
|
||||
path: file.path.clone(),
|
||||
})?;
|
||||
|
||||
let root_instance = temp_tree.get_instance(root_id).unwrap();
|
||||
let root_instance = temp_tree.get_instance(temp_tree.get_root_id()).unwrap();
|
||||
let children = root_instance.get_children_ids();
|
||||
|
||||
match children.len() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use serde_derive::{Serialize, Deserialize};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
|
||||
@@ -10,8 +10,8 @@ use std::{
|
||||
str,
|
||||
};
|
||||
|
||||
use rbx_tree::{RbxTree, RbxId, RbxInstanceProperties, RbxValue};
|
||||
use serde_derive::{Serialize, Deserialize};
|
||||
use rbx_dom_weak::{RbxTree, RbxId, RbxInstanceProperties, RbxValue};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use crate::{
|
||||
path_map::PathMap,
|
||||
@@ -64,7 +64,7 @@ impl InstanceChanges {
|
||||
|
||||
/// A lightweight, hierarchical representation of an instance that can be
|
||||
/// applied to the tree.
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RbxSnapshotInstance<'a> {
|
||||
pub name: Cow<'a, str>,
|
||||
pub class_name: Cow<'a, str>,
|
||||
@@ -73,6 +73,22 @@ pub struct RbxSnapshotInstance<'a> {
|
||||
pub metadata: MetadataPerInstance,
|
||||
}
|
||||
|
||||
impl<'a> RbxSnapshotInstance<'a> {
|
||||
pub fn get_owned(&'a self) -> RbxSnapshotInstance<'static> {
|
||||
let children: Vec<RbxSnapshotInstance<'static>> = self.children.iter()
|
||||
.map(RbxSnapshotInstance::get_owned)
|
||||
.collect();
|
||||
|
||||
RbxSnapshotInstance {
|
||||
name: Cow::Owned(self.name.clone().into_owned()),
|
||||
class_name: Cow::Owned(self.class_name.clone().into_owned()),
|
||||
properties: self.properties.clone(),
|
||||
children,
|
||||
metadata: self.metadata.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialOrd for RbxSnapshotInstance<'a> {
|
||||
fn partial_cmp(&self, other: &RbxSnapshotInstance) -> Option<Ordering> {
|
||||
Some(self.name.cmp(&other.name)
|
||||
@@ -137,7 +153,7 @@ pub fn reify_subtree(
|
||||
instance_per_path: &mut PathMap<HashSet<RbxId>>,
|
||||
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
|
||||
changes: &mut InstanceChanges,
|
||||
) {
|
||||
) -> RbxId {
|
||||
let instance = reify_core(snapshot);
|
||||
let id = tree.insert_instance(instance, parent_id);
|
||||
|
||||
@@ -148,6 +164,8 @@ pub fn reify_subtree(
|
||||
for child in &snapshot.children {
|
||||
reify_subtree(child, tree, id, instance_per_path, metadata_per_instance, changes);
|
||||
}
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
fn reify_metadata(
|
||||
@@ -206,6 +224,9 @@ fn reify_core(snapshot: &RbxSnapshotInstance) -> RbxInstanceProperties {
|
||||
instance
|
||||
}
|
||||
|
||||
/// Updates the given instance to match the properties defined on the snapshot.
|
||||
///
|
||||
/// Returns whether any changes were applied.
|
||||
fn reconcile_instance_properties(instance: &mut RbxInstanceProperties, snapshot: &RbxSnapshotInstance) -> bool {
|
||||
let mut has_diffs = false;
|
||||
|
||||
@@ -263,6 +284,8 @@ fn reconcile_instance_properties(instance: &mut RbxInstanceProperties, snapshot:
|
||||
has_diffs
|
||||
}
|
||||
|
||||
/// Updates the children of the instance in the `RbxTree` to match the children
|
||||
/// of the `RbxSnapshotInstance`. Order will be updated to match.
|
||||
fn reconcile_instance_children(
|
||||
tree: &mut RbxTree,
|
||||
id: RbxId,
|
||||
@@ -271,12 +294,21 @@ fn reconcile_instance_children(
|
||||
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
|
||||
changes: &mut InstanceChanges,
|
||||
) {
|
||||
let mut visited_snapshot_indices = HashSet::new();
|
||||
|
||||
let mut children_to_update: Vec<(RbxId, &RbxSnapshotInstance)> = Vec::new();
|
||||
let mut children_to_add: Vec<&RbxSnapshotInstance> = Vec::new();
|
||||
// These lists are kept so that we can apply all the changes we figure out
|
||||
let mut children_to_maybe_update: Vec<(RbxId, &RbxSnapshotInstance)> = Vec::new();
|
||||
let mut children_to_add: Vec<(usize, &RbxSnapshotInstance)> = Vec::new();
|
||||
let mut children_to_remove: Vec<RbxId> = Vec::new();
|
||||
|
||||
// This map is used once we're done mutating children to sort them according
|
||||
// to the order specified in the snapshot. Without it, a snapshot with a new
|
||||
// child prepended will cause the RbxTree instance to have out-of-order
|
||||
// children and would make Rojo non-deterministic.
|
||||
let mut ids_to_snapshot_indices = HashMap::new();
|
||||
|
||||
// Since we have to enumerate the children of both the RbxTree instance and
|
||||
// our snapshot, we keep a set of the snapshot children we've seen.
|
||||
let mut visited_snapshot_indices = vec![false; snapshot.children.len()];
|
||||
|
||||
let children_ids = tree.get_instance(id).unwrap().get_children_ids();
|
||||
|
||||
// Find all instances that were removed or updated, which we derive by
|
||||
@@ -287,7 +319,7 @@ fn reconcile_instance_children(
|
||||
// Locate a matching snapshot for this instance
|
||||
let mut matching_snapshot = None;
|
||||
for (snapshot_index, child_snapshot) in snapshot.children.iter().enumerate() {
|
||||
if visited_snapshot_indices.contains(&snapshot_index) {
|
||||
if visited_snapshot_indices[snapshot_index] {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -295,7 +327,8 @@ fn reconcile_instance_children(
|
||||
// similar. This heuristic is similar to React's reconciliation
|
||||
// strategy.
|
||||
if child_snapshot.name == child_instance.name {
|
||||
visited_snapshot_indices.insert(snapshot_index);
|
||||
ids_to_snapshot_indices.insert(child_id, snapshot_index);
|
||||
visited_snapshot_indices[snapshot_index] = true;
|
||||
matching_snapshot = Some(child_snapshot);
|
||||
break;
|
||||
}
|
||||
@@ -303,26 +336,23 @@ fn reconcile_instance_children(
|
||||
|
||||
match matching_snapshot {
|
||||
Some(child_snapshot) => {
|
||||
children_to_update.push((child_instance.get_id(), child_snapshot));
|
||||
},
|
||||
children_to_maybe_update.push((child_instance.get_id(), child_snapshot));
|
||||
}
|
||||
None => {
|
||||
children_to_remove.push(child_instance.get_id());
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find all instancs that were added, which is just the snapshots we didn't
|
||||
// match up to existing instances above.
|
||||
for (snapshot_index, child_snapshot) in snapshot.children.iter().enumerate() {
|
||||
if !visited_snapshot_indices.contains(&snapshot_index) {
|
||||
children_to_add.push(child_snapshot);
|
||||
if !visited_snapshot_indices[snapshot_index] {
|
||||
children_to_add.push((snapshot_index, child_snapshot));
|
||||
}
|
||||
}
|
||||
|
||||
for child_snapshot in &children_to_add {
|
||||
reify_subtree(child_snapshot, tree, id, instance_per_path, metadata_per_instance, changes);
|
||||
}
|
||||
|
||||
// Apply all of our removals we gathered from our diff
|
||||
for child_id in &children_to_remove {
|
||||
if let Some(subtree) = tree.remove_instance(*child_id) {
|
||||
for id in subtree.iter_all_ids() {
|
||||
@@ -332,7 +362,18 @@ fn reconcile_instance_children(
|
||||
}
|
||||
}
|
||||
|
||||
for (child_id, child_snapshot) in &children_to_update {
|
||||
// Apply all of our children additions
|
||||
for (snapshot_index, child_snapshot) in &children_to_add {
|
||||
let id = reify_subtree(child_snapshot, tree, id, instance_per_path, metadata_per_instance, changes);
|
||||
ids_to_snapshot_indices.insert(id, *snapshot_index);
|
||||
}
|
||||
|
||||
// Apply any updates that might have updates
|
||||
for (child_id, child_snapshot) in &children_to_maybe_update {
|
||||
reconcile_subtree(tree, *child_id, child_snapshot, instance_per_path, metadata_per_instance, changes);
|
||||
}
|
||||
|
||||
// Apply the sort mapping defined by ids_to_snapshot_indices above
|
||||
let instance = tree.get_instance_mut(id).unwrap();
|
||||
instance.sort_children_unstable_by_key(|id| ids_to_snapshot_indices.get(&id).unwrap());
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
io::Write,
|
||||
path::Path,
|
||||
@@ -6,12 +7,13 @@ use std::{
|
||||
};
|
||||
|
||||
use log::warn;
|
||||
use rbx_tree::RbxId;
|
||||
use rbx_dom_weak::{RbxTree, RbxId};
|
||||
|
||||
use crate::{
|
||||
imfs::{Imfs, ImfsItem},
|
||||
rbx_session::RbxSession,
|
||||
web::PublicInstanceMetadata,
|
||||
web::api::PublicInstanceMetadata,
|
||||
rbx_session::MetadataPerInstance,
|
||||
};
|
||||
|
||||
static GRAPHVIZ_HEADER: &str = r#"
|
||||
@@ -53,42 +55,59 @@ pub fn graphviz_to_svg(source: &str) -> Option<String> {
|
||||
Some(String::from_utf8(output.stdout).expect("Failed to parse stdout as UTF-8"))
|
||||
}
|
||||
|
||||
pub struct VisualizeRbxTree<'a, 'b> {
|
||||
pub tree: &'a RbxTree,
|
||||
pub metadata: &'b HashMap<RbxId, MetadataPerInstance>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> fmt::Display for VisualizeRbxTree<'a, 'b> {
|
||||
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(output, "{}", GRAPHVIZ_HEADER)?;
|
||||
|
||||
visualize_instance(&self.tree, self.tree.get_root_id(), &self.metadata, output)?;
|
||||
|
||||
writeln!(output, "}}")
|
||||
}
|
||||
}
|
||||
|
||||
/// A Display wrapper struct to visualize an RbxSession as SVG.
|
||||
pub struct VisualizeRbxSession<'a>(pub &'a RbxSession);
|
||||
|
||||
impl<'a> fmt::Display for VisualizeRbxSession<'a> {
|
||||
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(output, "{}", GRAPHVIZ_HEADER)?;
|
||||
|
||||
visualize_rbx_node(self.0, self.0.get_tree().get_root_id(), output)?;
|
||||
|
||||
writeln!(output, "}}")?;
|
||||
|
||||
Ok(())
|
||||
writeln!(output, "{}", VisualizeRbxTree {
|
||||
tree: self.0.get_tree(),
|
||||
metadata: self.0.get_all_instance_metadata(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn visualize_rbx_node(session: &RbxSession, id: RbxId, output: &mut fmt::Formatter) -> fmt::Result {
|
||||
let node = session.get_tree().get_instance(id).unwrap();
|
||||
fn visualize_instance(
|
||||
tree: &RbxTree,
|
||||
id: RbxId,
|
||||
metadata: &HashMap<RbxId, MetadataPerInstance>,
|
||||
output: &mut fmt::Formatter,
|
||||
) -> fmt::Result {
|
||||
let instance = tree.get_instance(id).unwrap();
|
||||
|
||||
let mut node_label = format!("{}|{}|{}", node.name, node.class_name, id);
|
||||
let mut instance_label = format!("{}|{}|{}", instance.name, instance.class_name, id);
|
||||
|
||||
if let Some(session_metadata) = session.get_instance_metadata(id) {
|
||||
if let Some(session_metadata) = metadata.get(&id) {
|
||||
let metadata = PublicInstanceMetadata::from_session_metadata(session_metadata);
|
||||
node_label.push('|');
|
||||
node_label.push_str(&serde_json::to_string(&metadata).unwrap());
|
||||
instance_label.push('|');
|
||||
instance_label.push_str(&serde_json::to_string(&metadata).unwrap());
|
||||
}
|
||||
|
||||
node_label = node_label
|
||||
instance_label = instance_label
|
||||
.replace("\"", """)
|
||||
.replace("{", "\\{")
|
||||
.replace("}", "\\}");
|
||||
|
||||
writeln!(output, " \"{}\" [label=\"{}\"]", id, node_label)?;
|
||||
writeln!(output, " \"{}\" [label=\"{}\"]", id, instance_label)?;
|
||||
|
||||
for &child_id in node.get_children_ids() {
|
||||
for &child_id in instance.get_children_ids() {
|
||||
writeln!(output, " \"{}\" -> \"{}\"", id, child_id)?;
|
||||
visualize_rbx_node(session, child_id, output)?;
|
||||
visualize_instance(tree, child_id, metadata, output)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
//! Defines Rojo's web interface that all clients use to communicate with a
|
||||
//! running live-sync session.
|
||||
//! Defines Rojo's HTTP API, all under /api. These endpoints generally return
|
||||
//! JSON.
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{mpsc, Arc},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use serde_derive::{Serialize, Deserialize};
|
||||
use log::trace;
|
||||
use rouille::{
|
||||
self,
|
||||
router,
|
||||
use futures::{
|
||||
future::{self, IntoFuture},
|
||||
Future,
|
||||
sync::oneshot,
|
||||
};
|
||||
use hyper::{
|
||||
service::Service,
|
||||
header,
|
||||
StatusCode,
|
||||
Method,
|
||||
Body,
|
||||
Request,
|
||||
Response,
|
||||
};
|
||||
use rbx_tree::{RbxId, RbxInstance};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use rbx_dom_weak::{RbxId, RbxInstance};
|
||||
|
||||
use crate::{
|
||||
live_session::LiveSession,
|
||||
session_id::SessionId,
|
||||
snapshot_reconciler::InstanceChanges,
|
||||
visualize::{VisualizeRbxSession, VisualizeImfs, graphviz_to_svg},
|
||||
rbx_session::{MetadataPerInstance},
|
||||
};
|
||||
|
||||
static HOME_CONTENT: &str = include_str!("../assets/index.html");
|
||||
|
||||
/// Contains the instance metadata relevant to Rojo clients.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -43,7 +47,7 @@ impl PublicInstanceMetadata {
|
||||
}
|
||||
|
||||
/// Used to attach metadata specific to Rojo to instances, which come from the
|
||||
/// rbx_tree crate.
|
||||
/// rbx_dom_weak crate.
|
||||
///
|
||||
/// Both fields are wrapped in Cow in order to make owned-vs-borrowed simpler
|
||||
/// for tests.
|
||||
@@ -82,121 +86,133 @@ pub struct SubscribeResponse<'a> {
|
||||
pub messages: Cow<'a, [InstanceChanges]>,
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
fn response_json<T: serde::Serialize>(value: T) -> Response<Body> {
|
||||
let serialized = match serde_json::to_string(&value) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
return Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.header(header::CONTENT_TYPE, "text/plain")
|
||||
.body(Body::from(err.to_string()))
|
||||
.unwrap();
|
||||
},
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(serialized))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub struct ApiService {
|
||||
live_session: Arc<LiveSession>,
|
||||
server_version: &'static str,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new(live_session: Arc<LiveSession>) -> Server {
|
||||
Server {
|
||||
impl Service for ApiService {
|
||||
type ReqBody = Body;
|
||||
type ResBody = Body;
|
||||
type Error = hyper::Error;
|
||||
type Future = Box<dyn Future<Item = hyper::Response<Self::ReqBody>, Error = Self::Error> + Send>;
|
||||
|
||||
fn call(&mut self, request: hyper::Request<Self::ReqBody>) -> Self::Future {
|
||||
let response = match (request.method(), request.uri().path()) {
|
||||
(&Method::GET, "/api/rojo") => self.handle_api_rojo(),
|
||||
(&Method::GET, path) if path.starts_with("/api/read/") => self.handle_api_read(request),
|
||||
(&Method::GET, path) if path.starts_with("/api/subscribe/") => {
|
||||
return self.handle_api_subscribe(request);
|
||||
}
|
||||
_ => {
|
||||
Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::empty())
|
||||
.unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
Box::new(future::ok(response))
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiService {
|
||||
pub fn new(live_session: Arc<LiveSession>) -> ApiService {
|
||||
ApiService {
|
||||
live_session,
|
||||
server_version: env!("CARGO_PKG_VERSION"),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
pub fn handle_request(&self, request: &Request) -> Response {
|
||||
trace!("Request {} {}", request.method(), request.url());
|
||||
|
||||
router!(request,
|
||||
(GET) (/) => {
|
||||
self.handle_home()
|
||||
},
|
||||
(GET) (/api/rojo) => {
|
||||
self.handle_api_rojo()
|
||||
},
|
||||
(GET) (/api/subscribe/{ cursor: u32 }) => {
|
||||
self.handle_api_subscribe(cursor)
|
||||
},
|
||||
(GET) (/api/read/{ id_list: String }) => {
|
||||
let requested_ids: Option<Vec<RbxId>> = id_list
|
||||
.split(',')
|
||||
.map(RbxId::parse_str)
|
||||
.collect();
|
||||
|
||||
self.handle_api_read(requested_ids)
|
||||
},
|
||||
(GET) (/visualize/rbx) => {
|
||||
self.handle_visualize_rbx()
|
||||
},
|
||||
(GET) (/visualize/imfs) => {
|
||||
self.handle_visualize_imfs()
|
||||
},
|
||||
_ => Response::empty_404()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn listen(self, port: u16) {
|
||||
let address = format!("0.0.0.0:{}", port);
|
||||
|
||||
rouille::start_server(address, move |request| self.handle_request(request));
|
||||
}
|
||||
|
||||
fn handle_home(&self) -> Response {
|
||||
Response::html(HOME_CONTENT)
|
||||
}
|
||||
|
||||
/// Get a summary of information about the server
|
||||
fn handle_api_rojo(&self) -> Response {
|
||||
fn handle_api_rojo(&self) -> Response<Body> {
|
||||
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
||||
let tree = rbx_session.get_tree();
|
||||
|
||||
Response::json(&ServerInfoResponse {
|
||||
response_json(&ServerInfoResponse {
|
||||
server_version: self.server_version,
|
||||
protocol_version: 2,
|
||||
session_id: self.live_session.session_id,
|
||||
expected_place_ids: self.live_session.project.serve_place_ids.clone(),
|
||||
session_id: self.live_session.session_id(),
|
||||
expected_place_ids: self.live_session.serve_place_ids().clone(),
|
||||
root_instance_id: tree.get_root_id(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieve any messages past the given cursor index, and if
|
||||
/// there weren't any, subscribe to receive any new messages.
|
||||
fn handle_api_subscribe(&self, cursor: u32) -> Response {
|
||||
fn handle_api_subscribe(&self, request: Request<Body>) -> <ApiService as Service>::Future {
|
||||
let argument = &request.uri().path()["/api/subscribe/".len()..];
|
||||
let cursor: u32 = match argument.parse() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
return Box::new(future::ok(Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.header(header::CONTENT_TYPE, "text/plain")
|
||||
.body(Body::from(err.to_string()))
|
||||
.unwrap()));
|
||||
},
|
||||
};
|
||||
|
||||
let message_queue = Arc::clone(&self.live_session.message_queue);
|
||||
let session_id = self.live_session.session_id();
|
||||
|
||||
// Did the client miss any messages since the last subscribe?
|
||||
{
|
||||
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
|
||||
let (tx, rx) = oneshot::channel();
|
||||
message_queue.subscribe(cursor, tx);
|
||||
|
||||
if !new_messages.is_empty() {
|
||||
return Response::json(&SubscribeResponse {
|
||||
session_id: self.live_session.session_id,
|
||||
messages: Cow::Borrowed(&new_messages),
|
||||
let result = rx.into_future()
|
||||
.and_then(move |(new_cursor, new_messages)| {
|
||||
Box::new(future::ok(response_json(SubscribeResponse {
|
||||
session_id: session_id,
|
||||
messages: Cow::Owned(new_messages),
|
||||
message_cursor: new_cursor,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
let sender_id = message_queue.subscribe(tx);
|
||||
|
||||
match rx.recv() {
|
||||
Ok(_) => (),
|
||||
Err(_) => return Response::text("error!").with_status_code(500),
|
||||
}
|
||||
|
||||
message_queue.unsubscribe(sender_id);
|
||||
|
||||
{
|
||||
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
|
||||
|
||||
return Response::json(&SubscribeResponse {
|
||||
session_id: self.live_session.session_id,
|
||||
messages: Cow::Owned(new_messages),
|
||||
message_cursor: new_cursor,
|
||||
})))
|
||||
})
|
||||
}
|
||||
.or_else(|e| {
|
||||
Box::new(future::ok(Response::builder()
|
||||
.status(500)
|
||||
.body(Body::from(format!("Internal Error: {:?}", e)))
|
||||
.unwrap()))
|
||||
});
|
||||
|
||||
Box::new(result)
|
||||
}
|
||||
|
||||
fn handle_api_read(&self, requested_ids: Option<Vec<RbxId>>) -> Response {
|
||||
fn handle_api_read(&self, request: Request<Body>) -> Response<Body> {
|
||||
let argument = &request.uri().path()["/api/read/".len()..];
|
||||
let requested_ids: Option<Vec<RbxId>> = argument
|
||||
.split(',')
|
||||
.map(RbxId::parse_str)
|
||||
.collect();
|
||||
|
||||
let message_queue = Arc::clone(&self.live_session.message_queue);
|
||||
|
||||
let requested_ids = match requested_ids {
|
||||
Some(id) => id,
|
||||
None => return rouille::Response::text("Malformed ID list").with_status_code(400),
|
||||
None => {
|
||||
return Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.header(header::CONTENT_TYPE, "text/plain")
|
||||
.body(Body::from("Malformed ID list"))
|
||||
.unwrap();
|
||||
},
|
||||
};
|
||||
|
||||
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
||||
@@ -228,30 +244,10 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
Response::json(&ReadResponse {
|
||||
session_id: self.live_session.session_id,
|
||||
response_json(&ReadResponse {
|
||||
session_id: self.live_session.session_id(),
|
||||
message_cursor,
|
||||
instances,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_visualize_rbx(&self) -> Response {
|
||||
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
||||
let dot_source = format!("{}", VisualizeRbxSession(&rbx_session));
|
||||
|
||||
match graphviz_to_svg(&dot_source) {
|
||||
Some(svg) => Response::svg(svg),
|
||||
None => Response::text(dot_source),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_visualize_imfs(&self) -> Response {
|
||||
let imfs = self.live_session.imfs.lock().unwrap();
|
||||
let dot_source = format!("{}", VisualizeImfs(&imfs));
|
||||
|
||||
match graphviz_to_svg(&dot_source) {
|
||||
Some(svg) => Response::svg(svg),
|
||||
None => Response::text(dot_source),
|
||||
}
|
||||
}
|
||||
}
|
||||
121
server/src/web/interface.rs
Normal file
121
server/src/web/interface.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
//! Defines the HTTP-based UI. These endpoints generally return HTML and SVG.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::{future, Future};
|
||||
use hyper::{
|
||||
service::Service,
|
||||
header,
|
||||
Body,
|
||||
Method,
|
||||
StatusCode,
|
||||
Request,
|
||||
Response,
|
||||
};
|
||||
use ritz::html;
|
||||
|
||||
use crate::{
|
||||
live_session::LiveSession,
|
||||
visualize::{VisualizeRbxSession, VisualizeImfs, graphviz_to_svg},
|
||||
};
|
||||
|
||||
static HOME_CSS: &str = include_str!("../../assets/index.css");
|
||||
|
||||
pub struct InterfaceService {
|
||||
live_session: Arc<LiveSession>,
|
||||
server_version: &'static str,
|
||||
}
|
||||
|
||||
impl Service for InterfaceService {
|
||||
type ReqBody = Body;
|
||||
type ResBody = Body;
|
||||
type Error = hyper::Error;
|
||||
type Future = Box<dyn Future<Item = Response<Self::ReqBody>, Error = Self::Error> + Send>;
|
||||
|
||||
fn call(&mut self, request: Request<Self::ReqBody>) -> Self::Future {
|
||||
let response = match (request.method(), request.uri().path()) {
|
||||
(&Method::GET, "/") => self.handle_home(),
|
||||
(&Method::GET, "/visualize/rbx") => self.handle_visualize_rbx(),
|
||||
(&Method::GET, "/visualize/imfs") => self.handle_visualize_imfs(),
|
||||
_ => Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
};
|
||||
|
||||
Box::new(future::ok(response))
|
||||
}
|
||||
}
|
||||
|
||||
impl InterfaceService {
|
||||
pub fn new(live_session: Arc<LiveSession>) -> InterfaceService {
|
||||
InterfaceService {
|
||||
live_session,
|
||||
server_version: env!("CARGO_PKG_VERSION"),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_home(&self) -> Response<Body> {
|
||||
let page = html! {
|
||||
<html>
|
||||
<head>
|
||||
<title>"Rojo"</title>
|
||||
<style>
|
||||
{ ritz::UnescapedText::new(HOME_CSS) }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="main">
|
||||
<h1 class="title">
|
||||
"Rojo Live Sync is up and running!"
|
||||
</h1>
|
||||
<h2 class="subtitle">
|
||||
"Version " { self.server_version }
|
||||
</h2>
|
||||
<a class="docs" href="https://lpghatguy.github.io/rojo">
|
||||
"Rojo Documentation"
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.header(header::CONTENT_TYPE, "text/html")
|
||||
.body(Body::from(format!("<!DOCTYPE html>{}", page)))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn handle_visualize_rbx(&self) -> Response<Body> {
|
||||
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
||||
let dot_source = format!("{}", VisualizeRbxSession(&rbx_session));
|
||||
|
||||
match graphviz_to_svg(&dot_source) {
|
||||
Some(svg) => Response::builder()
|
||||
.header(header::CONTENT_TYPE, "image/svg+xml")
|
||||
.body(Body::from(svg))
|
||||
.unwrap(),
|
||||
None => Response::builder()
|
||||
.header(header::CONTENT_TYPE, "text/plain")
|
||||
.body(Body::from(dot_source))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_visualize_imfs(&self) -> Response<Body> {
|
||||
let imfs = self.live_session.imfs.lock().unwrap();
|
||||
let dot_source = format!("{}", VisualizeImfs(&imfs));
|
||||
|
||||
match graphviz_to_svg(&dot_source) {
|
||||
Some(svg) => Response::builder()
|
||||
.header(header::CONTENT_TYPE, "image/svg+xml")
|
||||
.body(Body::from(svg))
|
||||
.unwrap(),
|
||||
None => Response::builder()
|
||||
.header(header::CONTENT_TYPE, "text/plain")
|
||||
.body(Body::from(dot_source))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
85
server/src/web/mod.rs
Normal file
85
server/src/web/mod.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
// TODO: This module needs to be public for visualize, we should move
|
||||
// PublicInstanceMetadata and switch this private!
|
||||
pub mod api;
|
||||
mod interface;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use log::trace;
|
||||
use futures::{
|
||||
future::{self, FutureResult},
|
||||
Future,
|
||||
};
|
||||
use hyper::{
|
||||
service::Service,
|
||||
Body,
|
||||
Request,
|
||||
Response,
|
||||
Server,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
live_session::LiveSession,
|
||||
};
|
||||
|
||||
use self::{
|
||||
api::ApiService,
|
||||
interface::InterfaceService,
|
||||
};
|
||||
|
||||
pub struct RootService {
|
||||
api: api::ApiService,
|
||||
interface: interface::InterfaceService,
|
||||
}
|
||||
|
||||
impl Service for RootService {
|
||||
type ReqBody = Body;
|
||||
type ResBody = Body;
|
||||
type Error = hyper::Error;
|
||||
type Future = Box<dyn Future<Item = Response<Self::ReqBody>, Error = Self::Error> + Send>;
|
||||
|
||||
fn call(&mut self, request: Request<Self::ReqBody>) -> Self::Future {
|
||||
trace!("{} {}", request.method(), request.uri().path());
|
||||
|
||||
if request.uri().path().starts_with("/api") {
|
||||
self.api.call(request)
|
||||
} else {
|
||||
self.interface.call(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RootService {
|
||||
pub fn new(live_session: Arc<LiveSession>) -> RootService {
|
||||
RootService {
|
||||
api: ApiService::new(Arc::clone(&live_session)),
|
||||
interface: InterfaceService::new(Arc::clone(&live_session)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LiveServer {
|
||||
live_session: Arc<LiveSession>,
|
||||
}
|
||||
|
||||
impl LiveServer {
|
||||
pub fn new(live_session: Arc<LiveSession>) -> LiveServer {
|
||||
LiveServer {
|
||||
live_session,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(self, port: u16) {
|
||||
let address = ([127, 0, 0, 1], port).into();
|
||||
|
||||
let server = Server::bind(&address)
|
||||
.serve(move || {
|
||||
let service: FutureResult<_, hyper::Error> =
|
||||
future::ok(RootService::new(Arc::clone(&self.live_session)));
|
||||
service
|
||||
})
|
||||
.map_err(|e| eprintln!("Server error: {}", e));
|
||||
|
||||
hyper::rt::run(server);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
use std::io::Read;
|
||||
|
||||
use rouille;
|
||||
use serde;
|
||||
use serde_json;
|
||||
|
||||
static MAX_BODY_SIZE: usize = 100 * 1024 * 1024; // 100 MiB
|
||||
|
||||
/// Pulls text that may be JSON out of a Rouille Request object.
|
||||
///
|
||||
/// Doesn't do any actual parsing -- all this method does is verify the content
|
||||
/// type of the request and read the request's body.
|
||||
fn read_json_text(request: &rouille::Request) -> Option<String> {
|
||||
// Bail out if the request body isn't marked as JSON
|
||||
let content_type = request.header("Content-Type")?;
|
||||
|
||||
if !content_type.starts_with("application/json") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let body = request.data()?;
|
||||
|
||||
// Allocate a buffer and read up to MAX_BODY_SIZE+1 bytes into it.
|
||||
let mut out = Vec::new();
|
||||
body.take(MAX_BODY_SIZE.saturating_add(1) as u64).read_to_end(&mut out).ok()?;
|
||||
|
||||
// If the body was too big (MAX_BODY_SIZE+1), we abort instead of trying to
|
||||
// process it.
|
||||
if out.len() > MAX_BODY_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
String::from_utf8(out).ok()
|
||||
}
|
||||
|
||||
/// Reads the body out of a Rouille Request and attempts to turn it into JSON.
|
||||
pub fn read_json<T>(request: &rouille::Request) -> Option<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
let body = read_json_text(&request)?;
|
||||
serde_json::from_str(&body).ok()?
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
collections::{HashMap, HashSet, BTreeSet},
|
||||
fs,
|
||||
path::PathBuf,
|
||||
};
|
||||
@@ -80,7 +80,7 @@ fn base_tree() -> Result<(TempDir, Imfs, ExpectedImfs, TestResources), Error> {
|
||||
expected_roots.insert(root.path().to_path_buf());
|
||||
|
||||
let root_item = {
|
||||
let mut children = HashSet::new();
|
||||
let mut children = BTreeSet::new();
|
||||
children.insert(foo_path.clone());
|
||||
children.insert(bar_path.clone());
|
||||
|
||||
@@ -91,7 +91,7 @@ fn base_tree() -> Result<(TempDir, Imfs, ExpectedImfs, TestResources), Error> {
|
||||
};
|
||||
|
||||
let foo_item = {
|
||||
let mut children = HashSet::new();
|
||||
let mut children = BTreeSet::new();
|
||||
children.insert(baz_path.clone());
|
||||
|
||||
ImfsItem::Directory(ImfsDirectory {
|
||||
@@ -199,7 +199,7 @@ fn adding_folder() -> Result<(), Error> {
|
||||
}
|
||||
|
||||
let folder_item = {
|
||||
let mut children = HashSet::new();
|
||||
let mut children = BTreeSet::new();
|
||||
children.insert(file1_path.clone());
|
||||
children.insert(file2_path.clone());
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#[macro_use] extern crate lazy_static;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::{HashMap, BTreeMap},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use rbx_tree::RbxValue;
|
||||
use rbx_dom_weak::RbxValue;
|
||||
|
||||
use librojo::{
|
||||
project::{Project, ProjectNode},
|
||||
@@ -53,7 +53,7 @@ fn single_partition_game() {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut replicated_storage_children = HashMap::new();
|
||||
let mut replicated_storage_children = BTreeMap::new();
|
||||
replicated_storage_children.insert("Foo".to_string(), foo);
|
||||
|
||||
let replicated_storage = ProjectNode {
|
||||
@@ -65,7 +65,7 @@ fn single_partition_game() {
|
||||
let mut http_service_properties = HashMap::new();
|
||||
http_service_properties.insert("HttpEnabled".to_string(), RbxValue::Bool {
|
||||
value: true,
|
||||
});
|
||||
}.into());
|
||||
|
||||
let http_service = ProjectNode {
|
||||
class_name: Some(String::from("HttpService")),
|
||||
@@ -73,7 +73,7 @@ fn single_partition_game() {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut root_children = HashMap::new();
|
||||
let mut root_children = BTreeMap::new();
|
||||
root_children.insert("ReplicatedStorage".to_string(), replicated_storage);
|
||||
root_children.insert("HttpService".to_string(), http_service);
|
||||
|
||||
@@ -86,6 +86,7 @@ fn single_partition_game() {
|
||||
Project {
|
||||
name: "single-sync-point".to_string(),
|
||||
tree: root_node,
|
||||
plugins: Vec::new(),
|
||||
serve_port: None,
|
||||
serve_place_ids: None,
|
||||
file_location: project_location.join("default.project.json"),
|
||||
|
||||
112
server/tests/snapshot_reconciler.rs
Normal file
112
server/tests/snapshot_reconciler.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
mod test_util;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use rbx_dom_weak::{RbxTree, RbxInstanceProperties};
|
||||
|
||||
use librojo::{
|
||||
snapshot_reconciler::{RbxSnapshotInstance, reconcile_subtree},
|
||||
};
|
||||
|
||||
use test_util::tree::trees_equal;
|
||||
|
||||
#[test]
|
||||
fn patch_communicativity() {
|
||||
let base_tree = RbxTree::new(RbxInstanceProperties {
|
||||
name: "DataModel".into(),
|
||||
class_name: "DataModel".into(),
|
||||
properties: HashMap::new(),
|
||||
});
|
||||
|
||||
let patch_a = RbxSnapshotInstance {
|
||||
name: "DataModel".into(),
|
||||
class_name: "DataModel".into(),
|
||||
children: vec![
|
||||
RbxSnapshotInstance {
|
||||
name: "Child-A".into(),
|
||||
class_name: "Folder".into(),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let patch_b = RbxSnapshotInstance {
|
||||
name: "DataModel".into(),
|
||||
class_name: "DataModel".into(),
|
||||
children: vec![
|
||||
RbxSnapshotInstance {
|
||||
name: "Child-B".into(),
|
||||
class_name: "Folder".into(),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let patch_combined = RbxSnapshotInstance {
|
||||
name: "DataModel".into(),
|
||||
class_name: "DataModel".into(),
|
||||
children: vec![
|
||||
RbxSnapshotInstance {
|
||||
name: "Child-A".into(),
|
||||
class_name: "Folder".into(),
|
||||
..Default::default()
|
||||
},
|
||||
RbxSnapshotInstance {
|
||||
name: "Child-B".into(),
|
||||
class_name: "Folder".into(),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let root_id = base_tree.get_root_id();
|
||||
|
||||
let mut tree_a = base_tree.clone();
|
||||
|
||||
reconcile_subtree(
|
||||
&mut tree_a,
|
||||
root_id,
|
||||
&patch_a,
|
||||
&mut Default::default(),
|
||||
&mut Default::default(),
|
||||
&mut Default::default(),
|
||||
);
|
||||
|
||||
reconcile_subtree(
|
||||
&mut tree_a,
|
||||
root_id,
|
||||
&patch_combined,
|
||||
&mut Default::default(),
|
||||
&mut Default::default(),
|
||||
&mut Default::default(),
|
||||
);
|
||||
|
||||
let mut tree_b = base_tree.clone();
|
||||
|
||||
reconcile_subtree(
|
||||
&mut tree_b,
|
||||
root_id,
|
||||
&patch_b,
|
||||
&mut Default::default(),
|
||||
&mut Default::default(),
|
||||
&mut Default::default(),
|
||||
);
|
||||
|
||||
reconcile_subtree(
|
||||
&mut tree_b,
|
||||
root_id,
|
||||
&patch_combined,
|
||||
&mut Default::default(),
|
||||
&mut Default::default(),
|
||||
&mut Default::default(),
|
||||
);
|
||||
|
||||
match trees_equal(&tree_a, &tree_b) {
|
||||
Ok(_) => {}
|
||||
Err(e) => panic!("{}", e),
|
||||
}
|
||||
}
|
||||
70
server/tests/snapshot_snapshots.rs
Normal file
70
server/tests/snapshot_snapshots.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
mod test_util;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use librojo::{
|
||||
imfs::Imfs,
|
||||
project::Project,
|
||||
rbx_snapshot::{SnapshotContext, snapshot_project_tree},
|
||||
};
|
||||
|
||||
use crate::test_util::{
|
||||
snapshot::*,
|
||||
};
|
||||
|
||||
macro_rules! generate_snapshot_tests {
|
||||
($($name: ident),*) => {
|
||||
$(
|
||||
paste::item! {
|
||||
#[test]
|
||||
fn [<snapshot_ $name>]() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let tests_folder = Path::new(env!("CARGO_MANIFEST_DIR")).join("../test-projects");
|
||||
let project_folder = tests_folder.join(stringify!($name));
|
||||
run_snapshot_test(&project_folder);
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
generate_snapshot_tests!(
|
||||
empty,
|
||||
json_model,
|
||||
localization,
|
||||
multi_partition_game,
|
||||
nested_partitions,
|
||||
single_partition_game,
|
||||
single_partition_model,
|
||||
transmute_partition
|
||||
);
|
||||
|
||||
fn run_snapshot_test(path: &Path) {
|
||||
println!("Running snapshot from project: {}", path.display());
|
||||
|
||||
let project = Project::load_fuzzy(path)
|
||||
.expect("Couldn't load project file for snapshot test");
|
||||
|
||||
let mut imfs = Imfs::new();
|
||||
imfs.add_roots_from_project(&project)
|
||||
.expect("Could not add IMFS roots to snapshot project");
|
||||
|
||||
let context = SnapshotContext {
|
||||
plugin_context: None,
|
||||
};
|
||||
|
||||
let mut snapshot = snapshot_project_tree(&context, &imfs, &project)
|
||||
.expect("Could not generate snapshot for snapshot test");
|
||||
|
||||
if let Some(snapshot) = snapshot.as_mut() {
|
||||
anonymize_snapshot(path, snapshot);
|
||||
}
|
||||
|
||||
match read_expected_snapshot(path) {
|
||||
Some(expected_snapshot) => assert_eq!(snapshot, expected_snapshot),
|
||||
None => write_expected_snapshot(path, &snapshot),
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use librojo::{
|
||||
imfs::Imfs,
|
||||
project::{Project, ProjectNode},
|
||||
rbx_snapshot::snapshot_project_tree,
|
||||
snapshot_reconciler::{RbxSnapshotInstance},
|
||||
};
|
||||
|
||||
macro_rules! generate_snapshot_tests {
|
||||
($($name: ident),*) => {
|
||||
$(
|
||||
paste::item! {
|
||||
#[test]
|
||||
fn [<snapshot_ $name>]() {
|
||||
let tests_folder = Path::new(env!("CARGO_MANIFEST_DIR")).join("../test-projects");
|
||||
let project_folder = tests_folder.join(stringify!($name));
|
||||
run_snapshot_test(&project_folder);
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
generate_snapshot_tests!(
|
||||
empty,
|
||||
nested_partitions,
|
||||
single_partition_game,
|
||||
single_partition_model,
|
||||
transmute_partition
|
||||
);
|
||||
|
||||
const SNAPSHOT_EXPECTED_NAME: &str = "expected-snapshot.json";
|
||||
|
||||
fn run_snapshot_test(path: &Path) {
|
||||
println!("Running snapshot from project: {}", path.display());
|
||||
|
||||
let project = Project::load_fuzzy(path)
|
||||
.expect("Couldn't load project file for snapshot test");
|
||||
|
||||
let mut imfs = Imfs::new();
|
||||
imfs.add_roots_from_project(&project)
|
||||
.expect("Could not add IMFS roots to snapshot project");
|
||||
|
||||
let mut snapshot = snapshot_project_tree(&imfs, &project)
|
||||
.expect("Could not generate snapshot for snapshot test");
|
||||
|
||||
if let Some(snapshot) = snapshot.as_mut() {
|
||||
anonymize_snapshot(path, snapshot);
|
||||
}
|
||||
|
||||
match read_expected_snapshot(path) {
|
||||
Some(expected_snapshot) => assert_eq!(snapshot, expected_snapshot),
|
||||
None => write_expected_snapshot(path, &snapshot),
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshots contain absolute paths, which simplifies much of Rojo.
|
||||
///
|
||||
/// For saving snapshots to the disk, we should strip off the project folder
|
||||
/// path to make them machine-independent. This doesn't work for paths that fall
|
||||
/// outside of the project folder, but that's okay here.
|
||||
///
|
||||
/// We also need to sort children, since Rojo tends to enumerate the filesystem
|
||||
/// in an unpredictable order.
|
||||
fn anonymize_snapshot(project_folder_path: &Path, snapshot: &mut RbxSnapshotInstance) {
|
||||
match snapshot.metadata.source_path.as_mut() {
|
||||
Some(path) => *path = anonymize_path(project_folder_path, path),
|
||||
None => {},
|
||||
}
|
||||
|
||||
match snapshot.metadata.project_definition.as_mut() {
|
||||
Some((_, project_node)) => anonymize_project_node(project_folder_path, project_node),
|
||||
None => {},
|
||||
}
|
||||
|
||||
snapshot.children.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
|
||||
for child in snapshot.children.iter_mut() {
|
||||
anonymize_snapshot(project_folder_path, child);
|
||||
}
|
||||
}
|
||||
|
||||
fn anonymize_project_node(project_folder_path: &Path, project_node: &mut ProjectNode) {
|
||||
match project_node.path.as_mut() {
|
||||
Some(path) => *path = anonymize_path(project_folder_path, path),
|
||||
None => {},
|
||||
}
|
||||
|
||||
for child_node in project_node.children.values_mut() {
|
||||
anonymize_project_node(project_folder_path, child_node);
|
||||
}
|
||||
}
|
||||
|
||||
fn anonymize_path(project_folder_path: &Path, path: &Path) -> PathBuf {
|
||||
if path.is_absolute() {
|
||||
path.strip_prefix(project_folder_path)
|
||||
.expect("Could not anonymize absolute path")
|
||||
.to_path_buf()
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
fn read_expected_snapshot(path: &Path) -> Option<Option<RbxSnapshotInstance<'static>>> {
|
||||
let contents = fs::read(path.join(SNAPSHOT_EXPECTED_NAME)).ok()?;
|
||||
let snapshot: Option<RbxSnapshotInstance<'static>> = serde_json::from_slice(&contents)
|
||||
.expect("Could not deserialize snapshot");
|
||||
|
||||
Some(snapshot)
|
||||
}
|
||||
|
||||
fn write_expected_snapshot(path: &Path, snapshot: &Option<RbxSnapshotInstance>) {
|
||||
let mut file = File::create(path.join(SNAPSHOT_EXPECTED_NAME))
|
||||
.expect("Could not open file to write snapshot");
|
||||
|
||||
serde_json::to_writer_pretty(&mut file, snapshot)
|
||||
.expect("Could not serialize snapshot to file");
|
||||
}
|
||||
@@ -1,31 +1,13 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::fs::{create_dir, copy};
|
||||
use std::path::Path;
|
||||
use std::io;
|
||||
|
||||
use rouille::Request;
|
||||
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use librojo::web::Server;
|
||||
|
||||
pub trait HttpTestUtil {
|
||||
fn get_string(&self, url: &str) -> String;
|
||||
}
|
||||
|
||||
impl HttpTestUtil for Server {
|
||||
fn get_string(&self, url: &str) -> String {
|
||||
let info_request = Request::fake_http("GET", url, vec![], vec![]);
|
||||
let response = self.handle_request(&info_request);
|
||||
|
||||
assert_eq!(response.status_code, 200);
|
||||
|
||||
let (mut reader, _) = response.data.into_reader_and_size();
|
||||
let mut body = String::new();
|
||||
reader.read_to_string(&mut body).unwrap();
|
||||
|
||||
body
|
||||
}
|
||||
}
|
||||
pub mod snapshot;
|
||||
pub mod tree;
|
||||
|
||||
pub fn copy_recursive(from: &Path, to: &Path) -> io::Result<()> {
|
||||
for entry in WalkDir::new(from) {
|
||||
@@ -51,4 +33,4 @@ pub fn copy_recursive(from: &Path, to: &Path) -> io::Result<()> {
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
79
server/tests/test_util/snapshot.rs
Normal file
79
server/tests/test_util/snapshot.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use librojo::{
|
||||
project::ProjectNode,
|
||||
snapshot_reconciler::RbxSnapshotInstance,
|
||||
rbx_session::MetadataPerInstance,
|
||||
};
|
||||
|
||||
const SNAPSHOT_EXPECTED_NAME: &str = "expected-snapshot.json";
|
||||
|
||||
/// Snapshots contain absolute paths, which simplifies much of Rojo.
|
||||
///
|
||||
/// For saving snapshots to the disk, we should strip off the project folder
|
||||
/// path to make them machine-independent. This doesn't work for paths that fall
|
||||
/// outside of the project folder, but that's okay here.
|
||||
///
|
||||
/// We also need to sort children, since Rojo tends to enumerate the filesystem
|
||||
/// in an unpredictable order.
|
||||
pub fn anonymize_snapshot(project_folder_path: &Path, snapshot: &mut RbxSnapshotInstance) {
|
||||
anonymize_metadata(project_folder_path, &mut snapshot.metadata);
|
||||
|
||||
snapshot.children.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
|
||||
for child in snapshot.children.iter_mut() {
|
||||
anonymize_snapshot(project_folder_path, child);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn anonymize_metadata(project_folder_path: &Path, metadata: &mut MetadataPerInstance) {
|
||||
match metadata.source_path.as_mut() {
|
||||
Some(path) => *path = anonymize_path(project_folder_path, path),
|
||||
None => {},
|
||||
}
|
||||
|
||||
match metadata.project_definition.as_mut() {
|
||||
Some((_, project_node)) => anonymize_project_node(project_folder_path, project_node),
|
||||
None => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn anonymize_project_node(project_folder_path: &Path, project_node: &mut ProjectNode) {
|
||||
match project_node.path.as_mut() {
|
||||
Some(path) => *path = anonymize_path(project_folder_path, path),
|
||||
None => {},
|
||||
}
|
||||
|
||||
for child_node in project_node.children.values_mut() {
|
||||
anonymize_project_node(project_folder_path, child_node);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn anonymize_path(project_folder_path: &Path, path: &Path) -> PathBuf {
|
||||
if path.is_absolute() {
|
||||
path.strip_prefix(project_folder_path)
|
||||
.expect("Could not anonymize absolute path")
|
||||
.to_path_buf()
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_expected_snapshot(path: &Path) -> Option<Option<RbxSnapshotInstance<'static>>> {
|
||||
let contents = fs::read(path.join(SNAPSHOT_EXPECTED_NAME)).ok()?;
|
||||
let snapshot: Option<RbxSnapshotInstance<'static>> = serde_json::from_slice(&contents)
|
||||
.expect("Could not deserialize snapshot");
|
||||
|
||||
Some(snapshot)
|
||||
}
|
||||
|
||||
pub fn write_expected_snapshot(path: &Path, snapshot: &Option<RbxSnapshotInstance>) {
|
||||
let mut file = File::create(path.join(SNAPSHOT_EXPECTED_NAME))
|
||||
.expect("Could not open file to write snapshot");
|
||||
|
||||
serde_json::to_writer_pretty(&mut file, snapshot)
|
||||
.expect("Could not serialize snapshot to file");
|
||||
}
|
||||
351
server/tests/test_util/tree.rs
Normal file
351
server/tests/test_util/tree.rs
Normal file
@@ -0,0 +1,351 @@
|
||||
//! Defines a mechanism to compare two RbxTree objects and generate a useful
|
||||
//! diff if they aren't the same. These methods ignore IDs, which are randomly
|
||||
//! generated whenever a tree is constructed anyways. This makes matching up
|
||||
//! pairs of instances that should be the same potentially difficult.
|
||||
//!
|
||||
//! It relies on a couple different ideas:
|
||||
//! - Instances with the same name and class name are matched as the same
|
||||
//! instance. See basic_equal for this logic
|
||||
//! - A path of period-delimited names (like Roblox's GetFullName) should be
|
||||
//! enough to debug most issues. If it isn't, we can do something fun like
|
||||
//! generate GraphViz graphs.
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
fs::{self, File},
|
||||
hash::Hash,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use log::error;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use rbx_dom_weak::{RbxId, RbxTree};
|
||||
|
||||
use librojo::{
|
||||
rbx_session::MetadataPerInstance,
|
||||
live_session::LiveSession,
|
||||
visualize::{VisualizeRbxTree, graphviz_to_svg},
|
||||
};
|
||||
|
||||
use super::snapshot::anonymize_metadata;
|
||||
|
||||
/// Marks a 'step' in the test, which will snapshot the session's current
|
||||
/// RbxTree object and compare it against the saved snapshot if it exists.
|
||||
pub fn tree_step(step: &str, live_session: &LiveSession, source_path: &Path) {
|
||||
let rbx_session = live_session.rbx_session.lock().unwrap();
|
||||
let tree = rbx_session.get_tree();
|
||||
|
||||
let project_folder = live_session.root_project().folder_location();
|
||||
let metadata = rbx_session.get_all_instance_metadata()
|
||||
.iter()
|
||||
.map(|(key, meta)| {
|
||||
let mut meta = meta.clone();
|
||||
anonymize_metadata(project_folder, &mut meta);
|
||||
(*key, meta)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let tree_with_metadata = TreeWithMetadata {
|
||||
tree: Cow::Borrowed(&tree),
|
||||
metadata: Cow::Owned(metadata),
|
||||
};
|
||||
|
||||
match read_tree_by_name(source_path, step) {
|
||||
Some(expected) => match trees_and_metadata_equal(&expected, &tree_with_metadata) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("Trees at step '{}' were not equal.\n{}", step, e);
|
||||
|
||||
let expected_gv = format!("{}", VisualizeRbxTree {
|
||||
tree: &expected.tree,
|
||||
metadata: &expected.metadata,
|
||||
});
|
||||
|
||||
let actual_gv = format!("{}", VisualizeRbxTree {
|
||||
tree: &tree_with_metadata.tree,
|
||||
metadata: &tree_with_metadata.metadata,
|
||||
});
|
||||
|
||||
let output_dir = PathBuf::from("failed-snapshots");
|
||||
fs::create_dir_all(&output_dir)
|
||||
.expect("Could not create failed-snapshots directory");
|
||||
|
||||
let expected_basename = format!("{}-{}-expected", live_session.root_project().name, step);
|
||||
let actual_basename = format!("{}-{}-actual", live_session.root_project().name, step);
|
||||
|
||||
let mut expected_out = output_dir.join(expected_basename);
|
||||
let mut actual_out = output_dir.join(actual_basename);
|
||||
|
||||
match (graphviz_to_svg(&expected_gv), graphviz_to_svg(&actual_gv)) {
|
||||
(Some(expected_svg), Some(actual_svg)) => {
|
||||
expected_out.set_extension("svg");
|
||||
actual_out.set_extension("svg");
|
||||
|
||||
fs::write(&expected_out, expected_svg)
|
||||
.expect("Couldn't write expected SVG");
|
||||
|
||||
fs::write(&actual_out, actual_svg)
|
||||
.expect("Couldn't write actual SVG");
|
||||
}
|
||||
_ => {
|
||||
expected_out.set_extension("gv");
|
||||
actual_out.set_extension("gv");
|
||||
|
||||
fs::write(&expected_out, expected_gv)
|
||||
.expect("Couldn't write expected GV");
|
||||
|
||||
fs::write(&actual_out, actual_gv)
|
||||
.expect("Couldn't write actual GV");
|
||||
}
|
||||
}
|
||||
|
||||
error!("Output at {} and {}", expected_out.display(), actual_out.display());
|
||||
|
||||
panic!("Tree mismatch at step '{}'", step);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
write_tree_by_name(source_path, step, &tree_with_metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_cow_map<K: Clone + Eq + Hash, V: Clone>() -> Cow<'static, HashMap<K, V>> {
|
||||
Cow::Owned(HashMap::new())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct TreeWithMetadata<'a> {
|
||||
#[serde(flatten)]
|
||||
pub tree: Cow<'a, RbxTree>,
|
||||
|
||||
#[serde(default = "new_cow_map")]
|
||||
pub metadata: Cow<'a, HashMap<RbxId, MetadataPerInstance>>,
|
||||
}
|
||||
|
||||
fn read_tree_by_name(path: &Path, identifier: &str) -> Option<TreeWithMetadata<'static>> {
|
||||
let mut file_path = path.join(identifier);
|
||||
file_path.set_extension("tree.json");
|
||||
|
||||
let contents = fs::read(&file_path).ok()?;
|
||||
let tree: TreeWithMetadata = serde_json::from_slice(&contents)
|
||||
.expect("Could not deserialize tree");
|
||||
|
||||
Some(tree)
|
||||
}
|
||||
|
||||
fn write_tree_by_name(path: &Path, identifier: &str, tree: &TreeWithMetadata) {
|
||||
let mut file_path = path.join(identifier);
|
||||
file_path.set_extension("tree.json");
|
||||
|
||||
let mut file = File::create(file_path)
|
||||
.expect("Could not open file to write tree");
|
||||
|
||||
serde_json::to_writer_pretty(&mut file, tree)
|
||||
.expect("Could not serialize tree to file");
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TreeMismatch {
|
||||
pub path: Cow<'static, str>,
|
||||
pub detail: Cow<'static, str>,
|
||||
}
|
||||
|
||||
impl TreeMismatch {
|
||||
pub fn new<'a, A: Into<Cow<'a, str>>, B: Into<Cow<'a, str>>>(path: A, detail: B) -> TreeMismatch {
|
||||
TreeMismatch {
|
||||
path: Cow::Owned(path.into().into_owned()),
|
||||
detail: Cow::Owned(detail.into().into_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_parent(mut self, name: &str) -> TreeMismatch {
|
||||
self.path.to_mut().insert(0, '.');
|
||||
self.path.to_mut().insert_str(0, name);
|
||||
|
||||
TreeMismatch {
|
||||
path: self.path,
|
||||
detail: self.detail,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TreeMismatch {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(formatter, "Tree mismatch at path {}", self.path)?;
|
||||
writeln!(formatter, "{}", self.detail)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trees_equal(
|
||||
left_tree: &RbxTree,
|
||||
right_tree: &RbxTree,
|
||||
) -> Result<(), TreeMismatch> {
|
||||
let left = TreeWithMetadata {
|
||||
tree: Cow::Borrowed(left_tree),
|
||||
metadata: Cow::Owned(HashMap::new()),
|
||||
};
|
||||
|
||||
let right = TreeWithMetadata {
|
||||
tree: Cow::Borrowed(right_tree),
|
||||
metadata: Cow::Owned(HashMap::new()),
|
||||
};
|
||||
|
||||
trees_and_metadata_equal(&left, &right)
|
||||
}
|
||||
|
||||
fn trees_and_metadata_equal(
|
||||
left_tree: &TreeWithMetadata,
|
||||
right_tree: &TreeWithMetadata,
|
||||
) -> Result<(), TreeMismatch> {
|
||||
let left_id = left_tree.tree.get_root_id();
|
||||
let right_id = right_tree.tree.get_root_id();
|
||||
|
||||
instances_equal(left_tree, left_id, right_tree, right_id)
|
||||
}
|
||||
|
||||
fn instances_equal(
|
||||
left_tree: &TreeWithMetadata,
|
||||
left_id: RbxId,
|
||||
right_tree: &TreeWithMetadata,
|
||||
right_id: RbxId,
|
||||
) -> Result<(), TreeMismatch> {
|
||||
basic_equal(left_tree, left_id, right_tree, right_id)?;
|
||||
properties_equal(left_tree, left_id, right_tree, right_id)?;
|
||||
children_equal(left_tree, left_id, right_tree, right_id)?;
|
||||
metadata_equal(left_tree, left_id, right_tree, right_id)
|
||||
}
|
||||
|
||||
fn basic_equal(
|
||||
left_tree: &TreeWithMetadata,
|
||||
left_id: RbxId,
|
||||
right_tree: &TreeWithMetadata,
|
||||
right_id: RbxId,
|
||||
) -> Result<(), TreeMismatch> {
|
||||
let left_instance = left_tree.tree.get_instance(left_id)
|
||||
.expect("ID did not exist in left tree");
|
||||
|
||||
let right_instance = right_tree.tree.get_instance(right_id)
|
||||
.expect("ID did not exist in right tree");
|
||||
|
||||
if left_instance.name != right_instance.name {
|
||||
let message = format!("Name did not match ('{}' vs '{}')", left_instance.name, right_instance.name);
|
||||
|
||||
return Err(TreeMismatch::new(&left_instance.name, message));
|
||||
}
|
||||
|
||||
if left_instance.class_name != right_instance.class_name {
|
||||
let message = format!("Class name did not match ('{}' vs '{}')", left_instance.class_name, right_instance.class_name);
|
||||
|
||||
return Err(TreeMismatch::new(&left_instance.name, message));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn properties_equal(
|
||||
left_tree: &TreeWithMetadata,
|
||||
left_id: RbxId,
|
||||
right_tree: &TreeWithMetadata,
|
||||
right_id: RbxId,
|
||||
) -> Result<(), TreeMismatch> {
|
||||
let left_instance = left_tree.tree.get_instance(left_id)
|
||||
.expect("ID did not exist in left tree");
|
||||
|
||||
let right_instance = right_tree.tree.get_instance(right_id)
|
||||
.expect("ID did not exist in right tree");
|
||||
|
||||
let mut visited = HashSet::new();
|
||||
|
||||
for (key, left_value) in &left_instance.properties {
|
||||
visited.insert(key);
|
||||
|
||||
let right_value = right_instance.properties.get(key);
|
||||
|
||||
if Some(left_value) != right_value {
|
||||
let message = format!(
|
||||
"Property {}:\n\tLeft: {:?}\n\tRight: {:?}",
|
||||
key,
|
||||
Some(left_value),
|
||||
right_value,
|
||||
);
|
||||
|
||||
return Err(TreeMismatch::new(&left_instance.name, message));
|
||||
}
|
||||
}
|
||||
|
||||
for (key, right_value) in &right_instance.properties {
|
||||
if visited.contains(key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let left_value = left_instance.properties.get(key);
|
||||
|
||||
if left_value != Some(right_value) {
|
||||
let message = format!(
|
||||
"Property {}:\n\tLeft: {:?}\n\tRight: {:?}",
|
||||
key,
|
||||
left_value,
|
||||
Some(right_value),
|
||||
);
|
||||
|
||||
return Err(TreeMismatch::new(&left_instance.name, message));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn children_equal(
|
||||
left_tree: &TreeWithMetadata,
|
||||
left_id: RbxId,
|
||||
right_tree: &TreeWithMetadata,
|
||||
right_id: RbxId,
|
||||
) -> Result<(), TreeMismatch> {
|
||||
let left_instance = left_tree.tree.get_instance(left_id)
|
||||
.expect("ID did not exist in left tree");
|
||||
|
||||
let right_instance = right_tree.tree.get_instance(right_id)
|
||||
.expect("ID did not exist in right tree");
|
||||
|
||||
let left_children = left_instance.get_children_ids();
|
||||
let right_children = right_instance.get_children_ids();
|
||||
|
||||
if left_children.len() != right_children.len() {
|
||||
return Err(TreeMismatch::new(&left_instance.name, "Instances had different numbers of children"));
|
||||
}
|
||||
|
||||
for (left_child_id, right_child_id) in left_children.iter().zip(right_children) {
|
||||
instances_equal(left_tree, *left_child_id, right_tree, *right_child_id)
|
||||
.map_err(|e| e.add_parent(&left_instance.name))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn metadata_equal(
|
||||
left_tree: &TreeWithMetadata,
|
||||
left_id: RbxId,
|
||||
right_tree: &TreeWithMetadata,
|
||||
right_id: RbxId,
|
||||
) -> Result<(), TreeMismatch> {
|
||||
let left_meta = left_tree.metadata.get(&left_id);
|
||||
let right_meta = right_tree.metadata.get(&right_id);
|
||||
|
||||
if left_meta != right_meta {
|
||||
let left_instance = left_tree.tree.get_instance(left_id)
|
||||
.expect("Left instance didn't exist in tree");
|
||||
|
||||
let message = format!(
|
||||
"Metadata mismatch:\n\tLeft: {:?}\n\tRight: {:?}",
|
||||
left_meta,
|
||||
right_meta,
|
||||
);
|
||||
|
||||
return Err(TreeMismatch::new(&left_instance.name, message));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
68
server/tests/tree_snapshots.rs
Normal file
68
server/tests/tree_snapshots.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
mod test_util;
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
use librojo::{
|
||||
live_session::LiveSession,
|
||||
project::Project,
|
||||
};
|
||||
|
||||
use crate::test_util::{
|
||||
copy_recursive,
|
||||
tree::tree_step,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn multi_partition_game() {
|
||||
let _ = env_logger::try_init();
|
||||
let source_path = project_path("multi_partition_game");
|
||||
|
||||
let (dir, live_session) = start_session(&source_path);
|
||||
tree_step("initial", &live_session, &source_path);
|
||||
|
||||
let added_path = dir.path().join("a/added");
|
||||
fs::create_dir_all(&added_path)
|
||||
.expect("Couldn't create directory");
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
|
||||
tree_step("with_dir", &live_session, &source_path);
|
||||
|
||||
let moved_path = dir.path().join("b/added");
|
||||
fs::rename(&added_path, &moved_path)
|
||||
.expect("Couldn't rename directory");
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
|
||||
tree_step("with_moved_dir", &live_session, &source_path);
|
||||
}
|
||||
|
||||
/// Find the path to the given test project relative to the manifest.
|
||||
fn project_path(name: &str) -> PathBuf {
|
||||
let mut path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../test-projects");
|
||||
path.push(name);
|
||||
path
|
||||
}
|
||||
|
||||
/// Starts a new LiveSession for the project located at the given file path.
|
||||
fn start_session(source_path: &Path) -> (TempDir, LiveSession) {
|
||||
let dir = tempdir()
|
||||
.expect("Couldn't create temporary directory");
|
||||
|
||||
copy_recursive(&source_path, dir.path())
|
||||
.expect("Couldn't copy project to temporary directory");
|
||||
|
||||
let project = Arc::new(Project::load_fuzzy(dir.path())
|
||||
.expect("Couldn't load project from temp directory"));
|
||||
|
||||
let live_session = LiveSession::new(Arc::clone(&project))
|
||||
.expect("Couldn't start live session");
|
||||
|
||||
(dir, live_session)
|
||||
}
|
||||
6
test-projects/json_model/default.project.json
Normal file
6
test-projects/json_model/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "json_model",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
76
test-projects/json_model/expected-snapshot.json
Normal file
76
test-projects/json_model/expected-snapshot.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "json_model",
|
||||
"class_name": "Folder",
|
||||
"properties": {},
|
||||
"children": [
|
||||
{
|
||||
"name": "children",
|
||||
"class_name": "Folder",
|
||||
"properties": {},
|
||||
"children": [
|
||||
{
|
||||
"name": "The Child",
|
||||
"class_name": "StringValue",
|
||||
"properties": {},
|
||||
"children": [],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": null,
|
||||
"project_definition": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "src/children.model.json",
|
||||
"project_definition": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "explicit",
|
||||
"class_name": "StringValue",
|
||||
"properties": {
|
||||
"Value": {
|
||||
"Type": "String",
|
||||
"Value": "Hello, world!"
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "src/explicit.model.json",
|
||||
"project_definition": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "implicit",
|
||||
"class_name": "StringValue",
|
||||
"properties": {
|
||||
"Value": {
|
||||
"Type": "String",
|
||||
"Value": "What's happenin', Earth?"
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "src/implicit.model.json",
|
||||
"project_definition": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "src",
|
||||
"project_definition": [
|
||||
"json_model",
|
||||
{
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "src"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
10
test-projects/json_model/src/children.model.json
Normal file
10
test-projects/json_model/src/children.model.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Name": "children",
|
||||
"ClassName": "Folder",
|
||||
"Children": [
|
||||
{
|
||||
"Name": "The Child",
|
||||
"ClassName": "StringValue"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
test-projects/json_model/src/explicit.model.json
Normal file
11
test-projects/json_model/src/explicit.model.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"Name": "explicit",
|
||||
"ClassName": "StringValue",
|
||||
"Properties": {
|
||||
"Value": {
|
||||
"Type": "String",
|
||||
"Value": "Hello, world!"
|
||||
}
|
||||
},
|
||||
"Children": []
|
||||
}
|
||||
7
test-projects/json_model/src/implicit.model.json
Normal file
7
test-projects/json_model/src/implicit.model.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"Name": "implicit",
|
||||
"ClassName": "StringValue",
|
||||
"Properties": {
|
||||
"Value": "What's happenin', Earth?"
|
||||
}
|
||||
}
|
||||
6
test-projects/localization/default.project.json
Normal file
6
test-projects/localization/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "localization",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
69
test-projects/localization/expected-snapshot.json
Normal file
69
test-projects/localization/expected-snapshot.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "localization",
|
||||
"class_name": "Folder",
|
||||
"properties": {},
|
||||
"children": [
|
||||
{
|
||||
"name": "empty-column-bug-147",
|
||||
"class_name": "LocalizationTable",
|
||||
"properties": {
|
||||
"Contents": {
|
||||
"Type": "String",
|
||||
"Value": "[{\"key\":\"Language.Name\",\"source\":\"English\",\"values\":{}},{\"key\":\"Language.Region\",\"source\":\"United States\",\"values\":{}},{\"key\":\"Label.Thickness\",\"source\":\"Thickness\",\"values\":{}},{\"key\":\"Label.Opacity\",\"source\":\"Opacity\",\"values\":{}},{\"key\":\"Toolbar.Undo\",\"source\":\"Undo\",\"values\":{}},{\"key\":\"Toolbar.Redo\",\"source\":\"Redo\",\"values\":{}},{\"key\":\"Toolbar.Camera\",\"source\":\"Top-down camera\",\"values\":{}},{\"key\":\"Toolbar.Saves\",\"source\":\"Saved drawings\",\"values\":{}},{\"key\":\"Toolbar.Preferences\",\"source\":\"Settings\",\"values\":{}},{\"key\":\"Toolbar.Mode.Vector\",\"source\":\"Vector mode\",\"values\":{}},{\"key\":\"Toolbar.Mode.Pixel\",\"source\":\"Pixel mode\",\"values\":{}}]"
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "src/empty-column-bug-147.csv",
|
||||
"project_definition": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "integers-bug-145",
|
||||
"class_name": "LocalizationTable",
|
||||
"properties": {
|
||||
"Contents": {
|
||||
"Type": "String",
|
||||
"Value": "[{\"key\":\"Count\",\"example\":\"A number demonstrating issue 145\",\"source\":\"3\",\"values\":{\"es\":\"7\"}}]"
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "src/integers-bug-145.csv",
|
||||
"project_definition": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "normal",
|
||||
"class_name": "LocalizationTable",
|
||||
"properties": {
|
||||
"Contents": {
|
||||
"Type": "String",
|
||||
"Value": "[{\"key\":\"Ack\",\"example\":\"An exclamation of despair\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]"
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "src/normal.csv",
|
||||
"project_definition": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "src",
|
||||
"project_definition": [
|
||||
"localization",
|
||||
{
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "src"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
17
test-projects/localization/src/empty-column-bug-147.csv
Normal file
17
test-projects/localization/src/empty-column-bug-147.csv
Normal file
@@ -0,0 +1,17 @@
|
||||
,Key,Source,Context,Example
|
||||
,,,,
|
||||
Metadata,Language.Name,English,,
|
||||
,Language.Region,United States,,
|
||||
,,,,
|
||||
Options,Label.Thickness,Thickness,,
|
||||
,Label.Opacity,Opacity,,
|
||||
,,,,
|
||||
Toolbar,Toolbar.Undo,Undo,,
|
||||
,Toolbar.Redo,Redo,,
|
||||
,,,,
|
||||
,Toolbar.Camera,Top-down camera,,
|
||||
,Toolbar.Saves,Saved drawings,,
|
||||
,Toolbar.Preferences,Settings,,
|
||||
,,,,
|
||||
,Toolbar.Mode.Vector,Vector mode,,
|
||||
,Toolbar.Mode.Pixel,Pixel mode,,
|
||||
|
2
test-projects/localization/src/integers-bug-145.csv
Normal file
2
test-projects/localization/src/integers-bug-145.csv
Normal file
@@ -0,0 +1,2 @@
|
||||
Key,Source,Context,Example,es
|
||||
Count,3,,A number demonstrating issue 145,7
|
||||
|
2
test-projects/localization/src/normal.csv
Normal file
2
test-projects/localization/src/normal.csv
Normal file
@@ -0,0 +1,2 @@
|
||||
Key,Source,Context,Example,es
|
||||
Ack,Ack!,,An exclamation of despair,¡Ay!
|
||||
|
6
test-projects/malformed-stuff/default.project.json
Normal file
6
test-projects/malformed-stuff/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "malformed-stuff",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
2
test-projects/malformed-stuff/src/bad-model.model.json
Normal file
2
test-projects/malformed-stuff/src/bad-model.model.json
Normal file
@@ -0,0 +1,2 @@
|
||||
ahhh this isn't a JSON model
|
||||
bamboozled again
|
||||
1
test-projects/multi_partition_game/a/foo.txt
Normal file
1
test-projects/multi_partition_game/a/foo.txt
Normal file
@@ -0,0 +1 @@
|
||||
Hello world, from a/foo.txt
|
||||
1
test-projects/multi_partition_game/a/main.lua
Normal file
1
test-projects/multi_partition_game/a/main.lua
Normal file
@@ -0,0 +1 @@
|
||||
-- hello, from a/main.lua
|
||||
1
test-projects/multi_partition_game/b/something.lua
Normal file
1
test-projects/multi_partition_game/b/something.lua
Normal file
@@ -0,0 +1 @@
|
||||
-- b/something.lua
|
||||
21
test-projects/multi_partition_game/default.project.json
Normal file
21
test-projects/multi_partition_game/default.project.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "multi_partition_game",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"ReplicatedStorage": {
|
||||
"$className": "ReplicatedStorage",
|
||||
"Ack": {
|
||||
"$path": "a"
|
||||
},
|
||||
"Bar": {
|
||||
"$path": "b"
|
||||
}
|
||||
},
|
||||
"HttpService": {
|
||||
"$className": "HttpService",
|
||||
"$properties": {
|
||||
"HttpEnabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
212
test-projects/multi_partition_game/expected-snapshot.json
Normal file
212
test-projects/multi_partition_game/expected-snapshot.json
Normal file
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"name": "multi_partition_game",
|
||||
"class_name": "DataModel",
|
||||
"properties": {},
|
||||
"children": [
|
||||
{
|
||||
"name": "HttpService",
|
||||
"class_name": "HttpService",
|
||||
"properties": {
|
||||
"HttpEnabled": {
|
||||
"Type": "Bool",
|
||||
"Value": true
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": true,
|
||||
"source_path": null,
|
||||
"project_definition": [
|
||||
"HttpService",
|
||||
{
|
||||
"class_name": "HttpService",
|
||||
"children": {},
|
||||
"properties": {
|
||||
"HttpEnabled": {
|
||||
"Type": "Bool",
|
||||
"Value": true
|
||||
}
|
||||
},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ReplicatedStorage",
|
||||
"class_name": "ReplicatedStorage",
|
||||
"properties": {},
|
||||
"children": [
|
||||
{
|
||||
"name": "Ack",
|
||||
"class_name": "Folder",
|
||||
"properties": {},
|
||||
"children": [
|
||||
{
|
||||
"name": "foo",
|
||||
"class_name": "StringValue",
|
||||
"properties": {
|
||||
"Value": {
|
||||
"Type": "String",
|
||||
"Value": "Hello world, from a/foo.txt"
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "a/foo.txt",
|
||||
"project_definition": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "main",
|
||||
"class_name": "ModuleScript",
|
||||
"properties": {
|
||||
"Source": {
|
||||
"Type": "String",
|
||||
"Value": "-- hello, from a/main.lua"
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "a/main.lua",
|
||||
"project_definition": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "a",
|
||||
"project_definition": [
|
||||
"Ack",
|
||||
{
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "a"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Bar",
|
||||
"class_name": "Folder",
|
||||
"properties": {},
|
||||
"children": [
|
||||
{
|
||||
"name": "something",
|
||||
"class_name": "ModuleScript",
|
||||
"properties": {
|
||||
"Source": {
|
||||
"Type": "String",
|
||||
"Value": "-- b/something.lua"
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "b/something.lua",
|
||||
"project_definition": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "b",
|
||||
"project_definition": [
|
||||
"Bar",
|
||||
{
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "b"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": true,
|
||||
"source_path": null,
|
||||
"project_definition": [
|
||||
"ReplicatedStorage",
|
||||
{
|
||||
"class_name": "ReplicatedStorage",
|
||||
"children": {
|
||||
"Bar": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "b"
|
||||
},
|
||||
"Ack": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "a"
|
||||
}
|
||||
},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"ignore_unknown_instances": true,
|
||||
"source_path": null,
|
||||
"project_definition": [
|
||||
"multi_partition_game",
|
||||
{
|
||||
"class_name": "DataModel",
|
||||
"children": {
|
||||
"ReplicatedStorage": {
|
||||
"class_name": "ReplicatedStorage",
|
||||
"children": {
|
||||
"Bar": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "b"
|
||||
},
|
||||
"Ack": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "a"
|
||||
}
|
||||
},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
},
|
||||
"HttpService": {
|
||||
"class_name": "HttpService",
|
||||
"children": {},
|
||||
"properties": {
|
||||
"HttpEnabled": {
|
||||
"Type": "Bool",
|
||||
"Value": true
|
||||
}
|
||||
},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
242
test-projects/multi_partition_game/initial.tree.json
Normal file
242
test-projects/multi_partition_game/initial.tree.json
Normal file
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"instances": {
|
||||
"00f207b1-fc18-4088-a45e-caf8cd98f5dd": {
|
||||
"Name": "main",
|
||||
"ClassName": "ModuleScript",
|
||||
"Properties": {
|
||||
"Source": {
|
||||
"Type": "String",
|
||||
"Value": "-- hello, from a/main.lua"
|
||||
}
|
||||
},
|
||||
"Id": "00f207b1-fc18-4088-a45e-caf8cd98f5dd",
|
||||
"Children": [],
|
||||
"Parent": "14fed1a3-ba97-46a6-ae93-ac26bd9471df"
|
||||
},
|
||||
"14fed1a3-ba97-46a6-ae93-ac26bd9471df": {
|
||||
"Name": "Ack",
|
||||
"ClassName": "Folder",
|
||||
"Properties": {},
|
||||
"Id": "14fed1a3-ba97-46a6-ae93-ac26bd9471df",
|
||||
"Children": [
|
||||
"c55fd55c-258e-4a93-a63a-ea243038c9b9",
|
||||
"00f207b1-fc18-4088-a45e-caf8cd98f5dd"
|
||||
],
|
||||
"Parent": "99eefe5f-ef74-49e6-8a8b-c833e00ca56b"
|
||||
},
|
||||
"c910510c-37a8-4fd8-ae41-01169ccb739c": {
|
||||
"Name": "Bar",
|
||||
"ClassName": "Folder",
|
||||
"Properties": {},
|
||||
"Id": "c910510c-37a8-4fd8-ae41-01169ccb739c",
|
||||
"Children": [
|
||||
"71a95983-c856-4cf2-aee6-bd8a523e80e4"
|
||||
],
|
||||
"Parent": "99eefe5f-ef74-49e6-8a8b-c833e00ca56b"
|
||||
},
|
||||
"c55fd55c-258e-4a93-a63a-ea243038c9b9": {
|
||||
"Name": "foo",
|
||||
"ClassName": "StringValue",
|
||||
"Properties": {
|
||||
"Value": {
|
||||
"Type": "String",
|
||||
"Value": "Hello world, from a/foo.txt"
|
||||
}
|
||||
},
|
||||
"Id": "c55fd55c-258e-4a93-a63a-ea243038c9b9",
|
||||
"Children": [],
|
||||
"Parent": "14fed1a3-ba97-46a6-ae93-ac26bd9471df"
|
||||
},
|
||||
"71a95983-c856-4cf2-aee6-bd8a523e80e4": {
|
||||
"Name": "something",
|
||||
"ClassName": "ModuleScript",
|
||||
"Properties": {
|
||||
"Source": {
|
||||
"Type": "String",
|
||||
"Value": "-- b/something.lua"
|
||||
}
|
||||
},
|
||||
"Id": "71a95983-c856-4cf2-aee6-bd8a523e80e4",
|
||||
"Children": [],
|
||||
"Parent": "c910510c-37a8-4fd8-ae41-01169ccb739c"
|
||||
},
|
||||
"3b5af13f-c997-4009-915c-0810b0e83032": {
|
||||
"Name": "multi_partition_game",
|
||||
"ClassName": "DataModel",
|
||||
"Properties": {},
|
||||
"Id": "3b5af13f-c997-4009-915c-0810b0e83032",
|
||||
"Children": [
|
||||
"bf8e2d4f-33a0-42a0-8168-1b62d6ac050c",
|
||||
"99eefe5f-ef74-49e6-8a8b-c833e00ca56b"
|
||||
],
|
||||
"Parent": null
|
||||
},
|
||||
"bf8e2d4f-33a0-42a0-8168-1b62d6ac050c": {
|
||||
"Name": "HttpService",
|
||||
"ClassName": "HttpService",
|
||||
"Properties": {
|
||||
"HttpEnabled": {
|
||||
"Type": "Bool",
|
||||
"Value": true
|
||||
}
|
||||
},
|
||||
"Id": "bf8e2d4f-33a0-42a0-8168-1b62d6ac050c",
|
||||
"Children": [],
|
||||
"Parent": "3b5af13f-c997-4009-915c-0810b0e83032"
|
||||
},
|
||||
"99eefe5f-ef74-49e6-8a8b-c833e00ca56b": {
|
||||
"Name": "ReplicatedStorage",
|
||||
"ClassName": "ReplicatedStorage",
|
||||
"Properties": {},
|
||||
"Id": "99eefe5f-ef74-49e6-8a8b-c833e00ca56b",
|
||||
"Children": [
|
||||
"14fed1a3-ba97-46a6-ae93-ac26bd9471df",
|
||||
"c910510c-37a8-4fd8-ae41-01169ccb739c"
|
||||
],
|
||||
"Parent": "3b5af13f-c997-4009-915c-0810b0e83032"
|
||||
}
|
||||
},
|
||||
"root_id": "3b5af13f-c997-4009-915c-0810b0e83032",
|
||||
"metadata": {
|
||||
"00f207b1-fc18-4088-a45e-caf8cd98f5dd": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "a/main.lua",
|
||||
"project_definition": null
|
||||
},
|
||||
"bf8e2d4f-33a0-42a0-8168-1b62d6ac050c": {
|
||||
"ignore_unknown_instances": true,
|
||||
"source_path": null,
|
||||
"project_definition": [
|
||||
"HttpService",
|
||||
{
|
||||
"class_name": "HttpService",
|
||||
"children": {},
|
||||
"properties": {
|
||||
"HttpEnabled": {
|
||||
"Type": "Bool",
|
||||
"Value": true
|
||||
}
|
||||
},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"14fed1a3-ba97-46a6-ae93-ac26bd9471df": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "a",
|
||||
"project_definition": [
|
||||
"Ack",
|
||||
{
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "a"
|
||||
}
|
||||
]
|
||||
},
|
||||
"c55fd55c-258e-4a93-a63a-ea243038c9b9": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "a/foo.txt",
|
||||
"project_definition": null
|
||||
},
|
||||
"71a95983-c856-4cf2-aee6-bd8a523e80e4": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "b/something.lua",
|
||||
"project_definition": null
|
||||
},
|
||||
"c910510c-37a8-4fd8-ae41-01169ccb739c": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "b",
|
||||
"project_definition": [
|
||||
"Bar",
|
||||
{
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"99eefe5f-ef74-49e6-8a8b-c833e00ca56b": {
|
||||
"ignore_unknown_instances": true,
|
||||
"source_path": null,
|
||||
"project_definition": [
|
||||
"ReplicatedStorage",
|
||||
{
|
||||
"class_name": "ReplicatedStorage",
|
||||
"children": {
|
||||
"Ack": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "a"
|
||||
},
|
||||
"Bar": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "b"
|
||||
}
|
||||
},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"3b5af13f-c997-4009-915c-0810b0e83032": {
|
||||
"ignore_unknown_instances": true,
|
||||
"source_path": null,
|
||||
"project_definition": [
|
||||
"multi_partition_game",
|
||||
{
|
||||
"class_name": "DataModel",
|
||||
"children": {
|
||||
"HttpService": {
|
||||
"class_name": "HttpService",
|
||||
"children": {},
|
||||
"properties": {
|
||||
"HttpEnabled": {
|
||||
"Type": "Bool",
|
||||
"Value": true
|
||||
}
|
||||
},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
},
|
||||
"ReplicatedStorage": {
|
||||
"class_name": "ReplicatedStorage",
|
||||
"children": {
|
||||
"Ack": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "a"
|
||||
},
|
||||
"Bar": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "b"
|
||||
}
|
||||
},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
256
test-projects/multi_partition_game/with_dir.tree.json
Normal file
256
test-projects/multi_partition_game/with_dir.tree.json
Normal file
@@ -0,0 +1,256 @@
|
||||
{
|
||||
"instances": {
|
||||
"b48b369f-5706-4029-9fa6-90651a4910ea": {
|
||||
"Name": "added",
|
||||
"ClassName": "Folder",
|
||||
"Properties": {},
|
||||
"Id": "b48b369f-5706-4029-9fa6-90651a4910ea",
|
||||
"Children": [],
|
||||
"Parent": "14fed1a3-ba97-46a6-ae93-ac26bd9471df"
|
||||
},
|
||||
"00f207b1-fc18-4088-a45e-caf8cd98f5dd": {
|
||||
"Name": "main",
|
||||
"ClassName": "ModuleScript",
|
||||
"Properties": {
|
||||
"Source": {
|
||||
"Type": "String",
|
||||
"Value": "-- hello, from a/main.lua"
|
||||
}
|
||||
},
|
||||
"Id": "00f207b1-fc18-4088-a45e-caf8cd98f5dd",
|
||||
"Children": [],
|
||||
"Parent": "14fed1a3-ba97-46a6-ae93-ac26bd9471df"
|
||||
},
|
||||
"14fed1a3-ba97-46a6-ae93-ac26bd9471df": {
|
||||
"Name": "Ack",
|
||||
"ClassName": "Folder",
|
||||
"Properties": {},
|
||||
"Id": "14fed1a3-ba97-46a6-ae93-ac26bd9471df",
|
||||
"Children": [
|
||||
"b48b369f-5706-4029-9fa6-90651a4910ea",
|
||||
"c55fd55c-258e-4a93-a63a-ea243038c9b9",
|
||||
"00f207b1-fc18-4088-a45e-caf8cd98f5dd"
|
||||
],
|
||||
"Parent": "99eefe5f-ef74-49e6-8a8b-c833e00ca56b"
|
||||
},
|
||||
"c910510c-37a8-4fd8-ae41-01169ccb739c": {
|
||||
"Name": "Bar",
|
||||
"ClassName": "Folder",
|
||||
"Properties": {},
|
||||
"Id": "c910510c-37a8-4fd8-ae41-01169ccb739c",
|
||||
"Children": [
|
||||
"71a95983-c856-4cf2-aee6-bd8a523e80e4"
|
||||
],
|
||||
"Parent": "99eefe5f-ef74-49e6-8a8b-c833e00ca56b"
|
||||
},
|
||||
"c55fd55c-258e-4a93-a63a-ea243038c9b9": {
|
||||
"Name": "foo",
|
||||
"ClassName": "StringValue",
|
||||
"Properties": {
|
||||
"Value": {
|
||||
"Type": "String",
|
||||
"Value": "Hello world, from a/foo.txt"
|
||||
}
|
||||
},
|
||||
"Id": "c55fd55c-258e-4a93-a63a-ea243038c9b9",
|
||||
"Children": [],
|
||||
"Parent": "14fed1a3-ba97-46a6-ae93-ac26bd9471df"
|
||||
},
|
||||
"71a95983-c856-4cf2-aee6-bd8a523e80e4": {
|
||||
"Name": "something",
|
||||
"ClassName": "ModuleScript",
|
||||
"Properties": {
|
||||
"Source": {
|
||||
"Type": "String",
|
||||
"Value": "-- b/something.lua"
|
||||
}
|
||||
},
|
||||
"Id": "71a95983-c856-4cf2-aee6-bd8a523e80e4",
|
||||
"Children": [],
|
||||
"Parent": "c910510c-37a8-4fd8-ae41-01169ccb739c"
|
||||
},
|
||||
"3b5af13f-c997-4009-915c-0810b0e83032": {
|
||||
"Name": "multi_partition_game",
|
||||
"ClassName": "DataModel",
|
||||
"Properties": {},
|
||||
"Id": "3b5af13f-c997-4009-915c-0810b0e83032",
|
||||
"Children": [
|
||||
"bf8e2d4f-33a0-42a0-8168-1b62d6ac050c",
|
||||
"99eefe5f-ef74-49e6-8a8b-c833e00ca56b"
|
||||
],
|
||||
"Parent": null
|
||||
},
|
||||
"bf8e2d4f-33a0-42a0-8168-1b62d6ac050c": {
|
||||
"Name": "HttpService",
|
||||
"ClassName": "HttpService",
|
||||
"Properties": {
|
||||
"HttpEnabled": {
|
||||
"Type": "Bool",
|
||||
"Value": true
|
||||
}
|
||||
},
|
||||
"Id": "bf8e2d4f-33a0-42a0-8168-1b62d6ac050c",
|
||||
"Children": [],
|
||||
"Parent": "3b5af13f-c997-4009-915c-0810b0e83032"
|
||||
},
|
||||
"99eefe5f-ef74-49e6-8a8b-c833e00ca56b": {
|
||||
"Name": "ReplicatedStorage",
|
||||
"ClassName": "ReplicatedStorage",
|
||||
"Properties": {},
|
||||
"Id": "99eefe5f-ef74-49e6-8a8b-c833e00ca56b",
|
||||
"Children": [
|
||||
"14fed1a3-ba97-46a6-ae93-ac26bd9471df",
|
||||
"c910510c-37a8-4fd8-ae41-01169ccb739c"
|
||||
],
|
||||
"Parent": "3b5af13f-c997-4009-915c-0810b0e83032"
|
||||
}
|
||||
},
|
||||
"root_id": "3b5af13f-c997-4009-915c-0810b0e83032",
|
||||
"metadata": {
|
||||
"c55fd55c-258e-4a93-a63a-ea243038c9b9": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "a/foo.txt",
|
||||
"project_definition": null
|
||||
},
|
||||
"99eefe5f-ef74-49e6-8a8b-c833e00ca56b": {
|
||||
"ignore_unknown_instances": true,
|
||||
"source_path": null,
|
||||
"project_definition": [
|
||||
"ReplicatedStorage",
|
||||
{
|
||||
"class_name": "ReplicatedStorage",
|
||||
"children": {
|
||||
"Ack": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "a"
|
||||
},
|
||||
"Bar": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "b"
|
||||
}
|
||||
},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"71a95983-c856-4cf2-aee6-bd8a523e80e4": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "b/something.lua",
|
||||
"project_definition": null
|
||||
},
|
||||
"00f207b1-fc18-4088-a45e-caf8cd98f5dd": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "a/main.lua",
|
||||
"project_definition": null
|
||||
},
|
||||
"bf8e2d4f-33a0-42a0-8168-1b62d6ac050c": {
|
||||
"ignore_unknown_instances": true,
|
||||
"source_path": null,
|
||||
"project_definition": [
|
||||
"HttpService",
|
||||
{
|
||||
"class_name": "HttpService",
|
||||
"children": {},
|
||||
"properties": {
|
||||
"HttpEnabled": {
|
||||
"Type": "Bool",
|
||||
"Value": true
|
||||
}
|
||||
},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"b48b369f-5706-4029-9fa6-90651a4910ea": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "a/added",
|
||||
"project_definition": null
|
||||
},
|
||||
"3b5af13f-c997-4009-915c-0810b0e83032": {
|
||||
"ignore_unknown_instances": true,
|
||||
"source_path": null,
|
||||
"project_definition": [
|
||||
"multi_partition_game",
|
||||
{
|
||||
"class_name": "DataModel",
|
||||
"children": {
|
||||
"HttpService": {
|
||||
"class_name": "HttpService",
|
||||
"children": {},
|
||||
"properties": {
|
||||
"HttpEnabled": {
|
||||
"Type": "Bool",
|
||||
"Value": true
|
||||
}
|
||||
},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
},
|
||||
"ReplicatedStorage": {
|
||||
"class_name": "ReplicatedStorage",
|
||||
"children": {
|
||||
"Ack": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "a"
|
||||
},
|
||||
"Bar": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "b"
|
||||
}
|
||||
},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"c910510c-37a8-4fd8-ae41-01169ccb739c": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "b",
|
||||
"project_definition": [
|
||||
"Bar",
|
||||
{
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"14fed1a3-ba97-46a6-ae93-ac26bd9471df": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "a",
|
||||
"project_definition": [
|
||||
"Ack",
|
||||
{
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "a"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
256
test-projects/multi_partition_game/with_moved_dir.tree.json
Normal file
256
test-projects/multi_partition_game/with_moved_dir.tree.json
Normal file
@@ -0,0 +1,256 @@
|
||||
{
|
||||
"instances": {
|
||||
"866071d6-465a-4b88-8c63-07489d950916": {
|
||||
"Name": "added",
|
||||
"ClassName": "Folder",
|
||||
"Properties": {},
|
||||
"Id": "866071d6-465a-4b88-8c63-07489d950916",
|
||||
"Children": [],
|
||||
"Parent": "c910510c-37a8-4fd8-ae41-01169ccb739c"
|
||||
},
|
||||
"00f207b1-fc18-4088-a45e-caf8cd98f5dd": {
|
||||
"Name": "main",
|
||||
"ClassName": "ModuleScript",
|
||||
"Properties": {
|
||||
"Source": {
|
||||
"Type": "String",
|
||||
"Value": "-- hello, from a/main.lua"
|
||||
}
|
||||
},
|
||||
"Id": "00f207b1-fc18-4088-a45e-caf8cd98f5dd",
|
||||
"Children": [],
|
||||
"Parent": "14fed1a3-ba97-46a6-ae93-ac26bd9471df"
|
||||
},
|
||||
"14fed1a3-ba97-46a6-ae93-ac26bd9471df": {
|
||||
"Name": "Ack",
|
||||
"ClassName": "Folder",
|
||||
"Properties": {},
|
||||
"Id": "14fed1a3-ba97-46a6-ae93-ac26bd9471df",
|
||||
"Children": [
|
||||
"c55fd55c-258e-4a93-a63a-ea243038c9b9",
|
||||
"00f207b1-fc18-4088-a45e-caf8cd98f5dd"
|
||||
],
|
||||
"Parent": "99eefe5f-ef74-49e6-8a8b-c833e00ca56b"
|
||||
},
|
||||
"c910510c-37a8-4fd8-ae41-01169ccb739c": {
|
||||
"Name": "Bar",
|
||||
"ClassName": "Folder",
|
||||
"Properties": {},
|
||||
"Id": "c910510c-37a8-4fd8-ae41-01169ccb739c",
|
||||
"Children": [
|
||||
"866071d6-465a-4b88-8c63-07489d950916",
|
||||
"71a95983-c856-4cf2-aee6-bd8a523e80e4"
|
||||
],
|
||||
"Parent": "99eefe5f-ef74-49e6-8a8b-c833e00ca56b"
|
||||
},
|
||||
"c55fd55c-258e-4a93-a63a-ea243038c9b9": {
|
||||
"Name": "foo",
|
||||
"ClassName": "StringValue",
|
||||
"Properties": {
|
||||
"Value": {
|
||||
"Type": "String",
|
||||
"Value": "Hello world, from a/foo.txt"
|
||||
}
|
||||
},
|
||||
"Id": "c55fd55c-258e-4a93-a63a-ea243038c9b9",
|
||||
"Children": [],
|
||||
"Parent": "14fed1a3-ba97-46a6-ae93-ac26bd9471df"
|
||||
},
|
||||
"71a95983-c856-4cf2-aee6-bd8a523e80e4": {
|
||||
"Name": "something",
|
||||
"ClassName": "ModuleScript",
|
||||
"Properties": {
|
||||
"Source": {
|
||||
"Type": "String",
|
||||
"Value": "-- b/something.lua"
|
||||
}
|
||||
},
|
||||
"Id": "71a95983-c856-4cf2-aee6-bd8a523e80e4",
|
||||
"Children": [],
|
||||
"Parent": "c910510c-37a8-4fd8-ae41-01169ccb739c"
|
||||
},
|
||||
"3b5af13f-c997-4009-915c-0810b0e83032": {
|
||||
"Name": "multi_partition_game",
|
||||
"ClassName": "DataModel",
|
||||
"Properties": {},
|
||||
"Id": "3b5af13f-c997-4009-915c-0810b0e83032",
|
||||
"Children": [
|
||||
"bf8e2d4f-33a0-42a0-8168-1b62d6ac050c",
|
||||
"99eefe5f-ef74-49e6-8a8b-c833e00ca56b"
|
||||
],
|
||||
"Parent": null
|
||||
},
|
||||
"bf8e2d4f-33a0-42a0-8168-1b62d6ac050c": {
|
||||
"Name": "HttpService",
|
||||
"ClassName": "HttpService",
|
||||
"Properties": {
|
||||
"HttpEnabled": {
|
||||
"Type": "Bool",
|
||||
"Value": true
|
||||
}
|
||||
},
|
||||
"Id": "bf8e2d4f-33a0-42a0-8168-1b62d6ac050c",
|
||||
"Children": [],
|
||||
"Parent": "3b5af13f-c997-4009-915c-0810b0e83032"
|
||||
},
|
||||
"99eefe5f-ef74-49e6-8a8b-c833e00ca56b": {
|
||||
"Name": "ReplicatedStorage",
|
||||
"ClassName": "ReplicatedStorage",
|
||||
"Properties": {},
|
||||
"Id": "99eefe5f-ef74-49e6-8a8b-c833e00ca56b",
|
||||
"Children": [
|
||||
"14fed1a3-ba97-46a6-ae93-ac26bd9471df",
|
||||
"c910510c-37a8-4fd8-ae41-01169ccb739c"
|
||||
],
|
||||
"Parent": "3b5af13f-c997-4009-915c-0810b0e83032"
|
||||
}
|
||||
},
|
||||
"root_id": "3b5af13f-c997-4009-915c-0810b0e83032",
|
||||
"metadata": {
|
||||
"bf8e2d4f-33a0-42a0-8168-1b62d6ac050c": {
|
||||
"ignore_unknown_instances": true,
|
||||
"source_path": null,
|
||||
"project_definition": [
|
||||
"HttpService",
|
||||
{
|
||||
"class_name": "HttpService",
|
||||
"children": {},
|
||||
"properties": {
|
||||
"HttpEnabled": {
|
||||
"Type": "Bool",
|
||||
"Value": true
|
||||
}
|
||||
},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"c910510c-37a8-4fd8-ae41-01169ccb739c": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "b",
|
||||
"project_definition": [
|
||||
"Bar",
|
||||
{
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"866071d6-465a-4b88-8c63-07489d950916": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "b/added",
|
||||
"project_definition": null
|
||||
},
|
||||
"14fed1a3-ba97-46a6-ae93-ac26bd9471df": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "a",
|
||||
"project_definition": [
|
||||
"Ack",
|
||||
{
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "a"
|
||||
}
|
||||
]
|
||||
},
|
||||
"00f207b1-fc18-4088-a45e-caf8cd98f5dd": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "a/main.lua",
|
||||
"project_definition": null
|
||||
},
|
||||
"99eefe5f-ef74-49e6-8a8b-c833e00ca56b": {
|
||||
"ignore_unknown_instances": true,
|
||||
"source_path": null,
|
||||
"project_definition": [
|
||||
"ReplicatedStorage",
|
||||
{
|
||||
"class_name": "ReplicatedStorage",
|
||||
"children": {
|
||||
"Ack": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "a"
|
||||
},
|
||||
"Bar": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "b"
|
||||
}
|
||||
},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"71a95983-c856-4cf2-aee6-bd8a523e80e4": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "b/something.lua",
|
||||
"project_definition": null
|
||||
},
|
||||
"c55fd55c-258e-4a93-a63a-ea243038c9b9": {
|
||||
"ignore_unknown_instances": false,
|
||||
"source_path": "a/foo.txt",
|
||||
"project_definition": null
|
||||
},
|
||||
"3b5af13f-c997-4009-915c-0810b0e83032": {
|
||||
"ignore_unknown_instances": true,
|
||||
"source_path": null,
|
||||
"project_definition": [
|
||||
"multi_partition_game",
|
||||
{
|
||||
"class_name": "DataModel",
|
||||
"children": {
|
||||
"HttpService": {
|
||||
"class_name": "HttpService",
|
||||
"children": {},
|
||||
"properties": {
|
||||
"HttpEnabled": {
|
||||
"Type": "Bool",
|
||||
"Value": true
|
||||
}
|
||||
},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
},
|
||||
"ReplicatedStorage": {
|
||||
"class_name": "ReplicatedStorage",
|
||||
"children": {
|
||||
"Ack": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "a"
|
||||
},
|
||||
"Bar": {
|
||||
"class_name": null,
|
||||
"children": {},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": "b"
|
||||
}
|
||||
},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
},
|
||||
"properties": {},
|
||||
"ignore_unknown_instances": null,
|
||||
"path": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,7 @@
|
||||
"HttpService": {
|
||||
"$className": "HttpService",
|
||||
"$properties": {
|
||||
"HttpEnabled": {
|
||||
"Type": "Bool",
|
||||
"Value": true
|
||||
}
|
||||
"HttpEnabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
411
test-projects/terrain/Terrain.rbxmx
Normal file
411
test-projects/terrain/Terrain.rbxmx
Normal file
@@ -0,0 +1,411 @@
|
||||
<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
|
||||
<Meta name="ExplicitAutoJoints">true</Meta>
|
||||
<External>null</External>
|
||||
<External>nil</External>
|
||||
<Item class="Terrain" referent="RBX6DE6533CDF9645EFB1BBC5A35FCAD7A2">
|
||||
<Properties>
|
||||
<bool name="Anchored">true</bool>
|
||||
<float name="BackParamA">-0.5</float>
|
||||
<float name="BackParamB">0.5</float>
|
||||
<token name="BackSurface">0</token>
|
||||
<token name="BackSurfaceInput">0</token>
|
||||
<float name="BottomParamA">-0.5</float>
|
||||
<float name="BottomParamB">0.5</float>
|
||||
<token name="BottomSurface">4</token>
|
||||
<token name="BottomSurfaceInput">0</token>
|
||||
<CoordinateFrame name="CFrame">
|
||||
<X>0</X>
|
||||
<Y>0</Y>
|
||||
<Z>0</Z>
|
||||
<R00>1</R00>
|
||||
<R01>0</R01>
|
||||
<R02>0</R02>
|
||||
<R10>0</R10>
|
||||
<R11>1</R11>
|
||||
<R12>0</R12>
|
||||
<R20>0</R20>
|
||||
<R21>0</R21>
|
||||
<R22>1</R22>
|
||||
</CoordinateFrame>
|
||||
<bool name="CanCollide">true</bool>
|
||||
<bool name="CastShadow">true</bool>
|
||||
<int name="CollisionGroupId">0</int>
|
||||
<Color3uint8 name="Color3uint8">4288914085</Color3uint8>
|
||||
<PhysicalProperties name="CustomPhysicalProperties">
|
||||
<CustomPhysics>false</CustomPhysics>
|
||||
</PhysicalProperties>
|
||||
<float name="FrontParamA">-0.5</float>
|
||||
<float name="FrontParamB">0.5</float>
|
||||
<token name="FrontSurface">0</token>
|
||||
<token name="FrontSurfaceInput">0</token>
|
||||
<float name="LeftParamA">-0.5</float>
|
||||
<float name="LeftParamB">0.5</float>
|
||||
<token name="LeftSurface">0</token>
|
||||
<token name="LeftSurfaceInput">0</token>
|
||||
<bool name="Locked">true</bool>
|
||||
<bool name="Massless">false</bool>
|
||||
<token name="Material">256</token>
|
||||
<BinaryString name="MaterialColors"><![CDATA[AAAAAAAAan8/P39rf2Y/ilY+j35fi21PZmxvZbDqw8faiVpHOi4kHh4lZlw76JxKc3trhHta
|
||||
gcLgc4RKxr21zq2UlJSM]]></BinaryString>
|
||||
<string name="Name">Terrain</string>
|
||||
<BinaryString name="PhysicsGrid"><![CDATA[AgMAAADC//8A//8A//8A/P8DAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAD/AAD/AAD/AAH9AAAAAAAAAAAAAAABAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAP//AP//AP//Af/4
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAD/AAD/AAD/AAH4AAAAAAAA
|
||||
AAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAP//AP//AP//Af/4AAAAAAAAAAAAAAAB
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAA
|
||||
AAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAD/AAD/AAD/AAH4AAAAAAAAAAAAAAABAAAAAAAA
|
||||
AAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAABAP//AP//AP//Af/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAD/AAD/AAD/AAH4AAAAAAAAAAAAAAABAAAAAAAA
|
||||
AAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAP//AP//AP//Af/4AAAAAAAA
|
||||
AAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAB
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAA
|
||||
AAAAAAABAAD/AAD/AAD/AAH4AAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAB
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAABAP//AP//AP//Af/8AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAD/AAD/AAD/AAH9AAAAAAAAAAAAAAABAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAP//AP//AP//Af/5AAAAAAAAAAAAAAAB
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAA
|
||||
AAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA
|
||||
AAD/AAD/AAD/AAH5AAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAA
|
||||
AAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAP//AP//AP//Af/5AAAAAAAAAAAAAAABAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAABAAAAAAAAAAAAAAAAAAD/AAD/AAD/AAH5AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAB
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA
|
||||
AP//AP//AP//Af/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAABAAAAAAAAAAAAAAAAAAD/AAD/AAD/AAH+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAP//AP//AP//Af/+AAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA
|
||||
AAD/AAD/AAD/AAH+AAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAP//AP//
|
||||
AP//Af/+AAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAD/AAD/AAD/AAH+AAAAAAAAAAAAAAAB
|
||||
AAAAAAAAAAAAAAABAAAAAAAAAAA=]]></BinaryString>
|
||||
<float name="Reflectance">0</float>
|
||||
<float name="RightParamA">-0.5</float>
|
||||
<float name="RightParamB">0.5</float>
|
||||
<token name="RightSurface">0</token>
|
||||
<token name="RightSurfaceInput">0</token>
|
||||
<int name="RootPriority">0</int>
|
||||
<Vector3 name="RotVelocity">
|
||||
<X>0</X>
|
||||
<Y>0</Y>
|
||||
<Z>0</Z>
|
||||
</Vector3>
|
||||
<BinaryString name="SmoothGrid"><![CDATA[AQX///////////////+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4DMQqFCrYAdQuVC80KigA5CgoAM
|
||||
Qs5C60KpgA1CgULggAxC00LxQq6ADUKDQuKADELPQvxCx4AOQt6ADEK+QvpC1IAOQsuADEKk
|
||||
QutC1IAOQqyADEKlQu5C14AOQqmADEKbQvJC60KIgA1CmoAMQoZC6kLyQpyA/4D/gLtCukLH
|
||||
gBxC8oICgA5CgYALggNCsIAMQtoCgAuCA0LFgAyCAYALggNCzoALQoCCAYALggNC9IAMggGA
|
||||
C0LlggOADIIBgAtCyIIDgAxC3QKAC0K7ggOADELFAoALQqOCA0KCgAtCoQKAC0KAggNCpIAM
|
||||
AoD/gP+Aq0KtggFC1YAaQomCA0LJgA1C3YAKQsyCBIAMggGACkKzggSAC0LZggGACkKeggSA
|
||||
C0LcggGAC4IEgAtCroIBgAuCBEKOgAuCAYALggRCq4ALggGAC4IEQryAC4IBgAuCBELegAuC
|
||||
AYALQtyCBIALQs0CAAAAAAAAAAAAAAABgP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4ANQtdC7EKhgBxC2ELs
|
||||
QqKAHELYQvpCuoAcQs5C+0LHgBxCt0LwQsmAHEK2Qu9CyIAcQrNC+ELdgBxCpUL1QueAHEKV
|
||||
QudC3IAcQpdC6kLfgBxCnELzQuxCiIAbQpRC7ULpQomAG0KDQttC2IAcQo9C7ELrQo6AG0Ki
|
||||
QvRC50KAgBtCoULnQs6AHEKsQvJC2YAcQsBC/ELWgBxCuULkQrCAHELMQvhCw4AcQsRC+ULR
|
||||
gBxCt0L7QuCAHEKUQt1CyYDhSISAHUjYSOhIoYAbSIBI50j0SNeAF4IDQrWADAKADIIDQruA
|
||||
DAKADIIDQtuADELogAyCA0L1gAxCvYAMQuGCA4AMQpSADELdggOAGkLOggNChgBCjEKKAEKR
|
||||
QoKAAkKGQoKADkKzggNC6ULYQu5C6kLcQvBC6ELRQuBC6ELsQulC5ULXgAxCn4IDQvBC30L0
|
||||
Qu5C4ELzQvRC5UL1QvhC90L1QvJC5IAMQqKCA0KZQopCm0KUQohCl0KhQptCqkKowqIBQqBC
|
||||
k4AMQqSCA0KCgBlCmIIDQoaAGUKIggOAGkKRggNCj4AZQq+CA4AaQrmCA4AaQsSCA4AaQuaC
|
||||
A4AaQu+CAkLpgBqCA0LvgBpC8YIDgBpC1IIDgBpCqIIDgBtC3IIBQqGAvkjEiAFI+IAbiANI
|
||||
woARSJqAB4gESImAFEK+ggSAC0KoAoALQrKCBIALQocCgAtCnIIEgAwCgAyCBIAMAoAMggRC
|
||||
gYALQvOADIIEQo8AQodChgBCj4AGQteADIISgAyCEoAMghKADIISgAyCBELeQpFCoEKYQotC
|
||||
mUKrQq5CvkK3QqxCrUKsQp+ADEL3ggNC44AZQuSCA0LYgBlC74IDQuyAGYIEQtCAGYIEQrCA
|
||||
GYIEQqqAGYIEQpGAGYIEgBqCBIAaggRCjoAZggRCuoAZggRCpIAZQpSCA4AcQrNCoICASMRI
|
||||
2UiTgBuIA0iwgBlIxogEgBBIu0j5SNaABUjRiARI5oAIAAAAAAAAAAAAAAABgP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4AHSJaACEjnSPVI9Ei3gBBIv0jwSM6ABkiKSN1I1UjxSOlIhYAPSM1I90j6SLeA
|
||||
BUiZSO9I5kjXSPRIyYAPSLZI5Uj6SNyABUitSPdI4ki1SOpI6EidgA5Iwkj6SOtI9EiegARI
|
||||
vEj7SNkASNtI7kjNgA5Iz0j8SMdI9kjdgARIz0j4SL8ASKRI80jiSLSADUjZSPtIu0jfSO9I
|
||||
toADSNdI6EisgAFIuEj6SNtIloAMSN1I6kiZSMlI8EjXSIKAAkjbSO1In4ABSIVIxkj8SM+A
|
||||
DEjbSOhIl0iHSOpI8EitgAFIm0jzSOxIioACSKRI10j6SLuACkicSPNI7EiIAEi3SPtI3IAB
|
||||
SLBI8kjUgARIuUjjSPNIuYAJSJZI6kjggAFIi0jYSPhItwBIxUj6SMyABUjMSPZI6Ui4gAhI
|
||||
k0jmSNyAAkjASONI4EiCSNZI/Ei/gAZIxUj5SOZIrYAHSKJI9UjpSIKAAUiNSOdI5Ei1SNlI
|
||||
8kirgAdI4Uj8SOlIn4AGSJhI6EjZgANIv0j6SOFI7EjwSKCAB0ieSOFI70ilgAZInEjsSN2A
|
||||
A0iDSOBI8Uj3SOWACUiESJeACEiSSIWABEioSO5I+kjXgBxItUjbSLuA/4DzSIyIAkixgAWI
|
||||
BEj8gA5I+IgDSJmAA0iTiAVIqoANiARI6IADSKKIBoANSOaIBIADSMKIBkivgAxI64gESNyA
|
||||
AkjeiAeADIgGSIGAAYgDSOyIA0j3gAuIBkjdgAGIA0jTiARIyIADSIKABYgHSIgAiANIsEih
|
||||
iARInYACSKuABYgHSPBIoogDSIUASM+IBEiBgAEIgARIpIgDSP6IA0jNiAOAAkj5iASAAQiA
|
||||
BEifiANIp4gDSPOIA4ACSH+IBEjxAEi6gARInIgDAIgHSOeAA0iniARI+UiTgARIrogDAEiR
|
||||
iAZIw4AESKSIBEizgARIp4gDgAFI74gFSLiABUjdiANIuYAESKqIA4ABSKaIBYAHSOiIAoAG
|
||||
iAJI04ACSOGIBIAISItInoAISJZIiYAEiANI+4AbSPOIAUiCgP+A0kjqiAOABEjOiAVIloAN
|
||||
iARI+IADSPGIBoAMSIOIBYADiAdIqoAMiAVI1IACiAiADIgGgAKICEjOgAuIBkjdAEiGiAlI
|
||||
kIADSIaABEiciAcASLiICoADSN6ABEjGiAdI5UjTiApI/YACCIAESNqIDUjhAIgFSN0ASIII
|
||||
gASIDkiwAEitiAVI2QAIgASIDkiVgAFI24gFSMAIgARI/IgDSMuICIADiAZI8oAEiARI00jv
|
||||
iAeABIgGgASIBEi/SJWIB4AFiAWABIgESMIAiAZI2oAFSKWIA0jAgARIrogDgAKIBUiZgAdI
|
||||
6Ej+SLKABkikSPVI5oADSMSIBIAaSNGIAkjegBxIpUiGgP+ArQAA/wAA/wAA/wAB/oD/gP+A
|
||||
q0KuggFC1oAaQoqCA0LKgA1C3oAKQsyCBIAMggGACkK0ggSAC0LaggGACkKfggSAC0LdggGA
|
||||
CkKAggSAC0KvggGAC4IEQo+AC4IBgAuCBEKrgAuCAYALggRCvYALggGAC4IEQt+AC4IBgAtC
|
||||
3YIEgAtCzgKA/4D/gKxCvELJgBxC9YICgA5Cg4ALggNCsoAMQt0CgAuCA0LIgAtCgIIBgAuC
|
||||
A0LRgAtCgoIBgAuCA0L2gAyCAYALQueCA4AMggGAC0LLggOADELgAoALQr6CA4AMQsgCgAtC
|
||||
poIDQoSAC0KkAoALQoOCA0KmgAwCgP+A/4DMQqVCsoAcQoFC6kL4QqaADkKGgAxC0kLwQq2A
|
||||
DUKFQuWADELYQvVCsoANQodC54AMQtMCQsuADkLjgAxCwwJC2IAOQtCADEKoQu9C2YAOQrGA
|
||||
DEKpQvNC3IAOQq6ADEKgQvdC70KMgA1CnoAMQopC70L3QqCADUKCgP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/wAAAAAAAAAAAAAAAYALQr6CBIALQqkCgAtCs4IEgAtCiAKAC0KdggSADAKADIIEgAwCgAyC
|
||||
BEKCgAtC9IAMggRC20LKQuNC4kLUQuxC0UK7QrhCyULXQtJCy0LYgAyCEoAMghKADIISgAyC
|
||||
EoAMggVC7gJC9kLnQveCBkL+gAxC+IIDQuSAGULlggNC2YAZQu+CA0LtgBmCBELRgBmCBEKx
|
||||
gBmCBEKrgBmCBEKSgBmCBIAaggSAGoIEQo+AGYIEQruAGYIEQqWAGUKVggOAHEKzQqCAgEjF
|
||||
SNpIlIAbiANIsYAZSMeIBIAQSLxI+kjXgAVI0ogESOeAFYIDQreADAKADIIDQr6ADAKADIID
|
||||
Qt2ADELrgAyCA0L4gAxCwIAMQuSCA4AMQpeADELgggNC20LKQuNC4kLUQuxC0UK7QrhCyULX
|
||||
QtJCy0K+gAxC0IIRgAxCtYIRgAxCooIRgAxCpYIRgAxCpoIEQu4CQvZC50L3ggZC/oAMQpqC
|
||||
A0KJgBlCioIDQn+AGUKTggNCkYAZQrGCA4AaQryCA4AaQseCA4AaQuiCA4AaQvGCAkLsgBqC
|
||||
A0LygBpC84IDgBpC1oIDgBpCqoIDgBtC3oIBQqSAoEiAgBxIx4gBSPuAG4gDSMSAEUidgAeI
|
||||
BEiMgBZC20LwQqWAHELcQvFCpoAcQt0CQr6AHELSAkLLgBxCu0L1Qs2AHEK6QvRCzABCgABC
|
||||
h0KGAEKPgBVCt0L9QuFC84INgA1CqUL6QuuCDoANQplC7ELggg6ADUKbQu9C44IOgA1CoEL4
|
||||
QvBCjEKgQpFCoEKYQotCmUKrQq5CvkK3QqxCrUKsQp+ADUKYQvJC7kKNgBtCh0LfQtxCgYAb
|
||||
QpPC8AFCkoAbQqZC+ULsQoSAG0KlQutC0oAcQrBC90LdgBxCxAJC2oAcQr1C6EK0gBxC0EL9
|
||||
QseAHELIQv5C1YAcQrsCQuWAHEKZQuJCzoDhSIiAHUjcSO1IpYAbSIRI7Ej5SNuA3EKGAEKM
|
||||
QooAQpFCgoACQoZCgoASQptC6ULYQu5C6kLcQvBC6ELRQuBC6ELsQulC5ELXgBBCokLwQt9C
|
||||
9ELuQuBC80L0QuVC9UL4QvdC9ULyQuSAEUKZQopCm0KUQohCl0KhQptCqkKowqIBQqBCk4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4C/AAAAAAAAAAAAAAABgAVI64gDgARIz4gFSJeADYgESPmAA0jyiAaADEiE
|
||||
iAWAA4gHSKuADIgFSNWAAogIgAyIBoACiAhIz4ALiAZI3gBIh4gJSJGAA0iHgARInYgHAEi5
|
||||
iAqAA0jfgARIx4gHSOZI1IgKSP6AAgiABEjbiA1I4gCIBUjeAEiDCIAEiA5IsQBIrogFSNoA
|
||||
CIAEiA5IloABSNyIBUjBCIAESP2IA0jMiAiAA4gGSPOABIgESNRI8IgHgASIBoAEiARIwEiW
|
||||
iAeABYgFgASIBEjDAIgGSNuABUimiANIwYAESK+IA4ACiAVImoAHSOkISLOABkilSPZI54AD
|
||||
SMWIBEiAgBlI0ogCSN+AHEimSIeA/4CzSI+IAkizgAWIBYAOSPuIA0icgANIlYgFSK2ADYgE
|
||||
SOuAA0iliAaADUjpiASAA0jEiAZIsYAMSO6IBEjfgAJI4IgHgAyIBkiEgAGIA0jviANI+oAL
|
||||
iAZI4IABiANI1ogESMuAA0iFgAWIB0iLAIgDSLNIo4gESKCAAkiugARIgYgHSPNIpIgDSIcA
|
||||
SNKIBEiEgAEIgARIpogISNCIA4ACSPyIBEiAAAiABEiiiANIqYgDSPaIA4ACSIKIBEjzAEi+
|
||||
gARIn4gDAIgHSOqAA0ipiARI/EiXgARIsYgDAEiTiAZIxoAESKeIBEi2gARIqYgDgAFI8YgF
|
||||
SLuABUjfiANIvIAESK2IA4ABSKiIBUiBgAZI64gCgAaIAkjWgAJI5IgEgAhIjUiggAhImUiM
|
||||
gASIA0j+gBtI9ogBSISA/4DUSJqAB0iDSOxI+kj5SLyAEEjDSPVI0kiCgAVIjkjiSNpI9kjt
|
||||
SImAD0jSSPwISLuABUieSPRI60jcSPlIzYAPSLpI6ghI4YAFSLFI/EjmSLpI70jtSKGADkjG
|
||||
CEjwSPlIooAESMAISN4ASOBI80jSgA5I0whIzEj7SOGABEjUSP1IxABIqUj4SOdIuIANSN4I
|
||||
SMBI40j0SLqAA0jcSO1IsIABSLwISOBImoAMSOJI70idSM1I9UjcSIaAAkjgSPJIpIABSIlI
|
||||
ywhI00iCgAtI4EjtSJxIjEjuSPVIsoABSJ9I+EjxSI6AAkioSNsISL+ACkigSPhI8EiMAEi8
|
||||
CEjhgAFItEj3SNiABEi+SOhI+Ei9gAlImkjuSOVIgABIj0jcSP1IuwBIyghI0YAFSNBI+0jt
|
||||
SL2ACEiXSOtI4YACSMRI6EjlSIZI2whIxIAGSMpI/kjqSLKAB0inSPpI7kiGgAFIkUjsSOlI
|
||||
ukjeSPdIsIAGSIJI5QhI7UijgAZInUjtSN6AA0jDCEjmSPFI9UikgAdIokjmSPRIqYAGSKBI
|
||||
8EjigANIh0jlSPZI/EjqSH+ACEiJSJuACEiWSImABEitSPMISNyAHEi6SOBIv4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4DtAP//AP//AP//Af/+gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+AlkKAQs5CwYAc
|
||||
Qs9C9kLoQoCAGkKIQuxC80K7gAVChoAVQtBC2EKFgAVC5EKMgB1C50KTgB1C+EKygB1C+0LJ
|
||||
gB1C70LSgB1C7ELQgB1C8ULqQoaAHELnQvZCpID/gP+Ak0K2AkL9QpKAGkKyggOAGoIEgARC
|
||||
hYATQoKCBIAEAkLngBOCA0KOgASCAUKRgBJCsYIBQsmABYIBQqyAHIIBQsuAHIIBQvmAHIIC
|
||||
gByCAoAcggJCgYAbggJCs4D/gP+Ac0KIgB2CAkLwgBqCBEKqgBhCsoIEQs+AA0LhQomAEkLe
|
||||
ggRCmIADggFCj4ARQsGCA0LrgASCAULvgBKCA4AFggKAE0KjQquABoICgByCAoAcggJCnoAb
|
||||
ggJCwoAbggJC3YAbggOAGwAAAAAAAAAAAAAAAYD/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP9Cz0LzQraAHELH
|
||||
QupCuoAcQr9C+ELQgBxCqkL3QuSAFEKiQp6ABUKJQuhC60KQgBJClELtQulCiIAFQtpC4kKc
|
||||
gBJCl0LlQtaABkLbQvRCrIASQp5C7ULegAZCzkL8QsiAEkKyQvhC3oAGQrRC9kLXgBJCvUL6
|
||||
QtWABkKiQuhC3YASQsFC80LEgAZClkLoQt5CiIARQrtC7EK+gAZCg0LnQu9CmoARQtBC+EK+
|
||||
gAdC3UL6QrWAEULcQvpCtoAHQsxC/ELKgBFC00L2QriAB0K2QvhC2IARQsJC6EKwgAdCn0Lt
|
||||
QuGAEULNQvVCvIAHQpNC5ELjQo6AEELOQvxCyIAHQoFC40LpQpyAEELGQvlCyoAIQttC9UKv
|
||||
gBBCv0L2QsuACELOQvtCxoAQQrdC8kLMgAhCvUL7QtmAEEK0QvNC0YAIQpRC0EKvgBBCsUL0
|
||||
QtaAHEKlQvJC6UKEgBxCsEKngGFItkimgBxIpkj1SORIgoAbSMdI90j5SMSAA0iJSKhIvUjF
|
||||
SLZIn0iLgBBIy0jkSPNI6UiNgAJI3kj1SPtI/Ej3SN5I6kjgSM9IsEiVgAtIhkjdSPNI2Ej7
|
||||
SLyAAkjXSOdI10jQSNhI1kjtSPhI+0j3SOlI10ifgAlIp0jtSOdIrEjtSPCCAkLZgBuCAkLz
|
||||
gBuCA4ATQrZCsoAFggOAEoICQvqABIIDQpOAEEKZggNChYADggNCs4AQQqaCA4AEggNCx4AQ
|
||||
Qq2CA4AEggNC94AQQsuCA4AEQvaCA4AQQuKCA4AEQsCCA4AQQvCCAkL3gARCoYIDQpGAD0L1
|
||||
ggJC8YAFggNCrIAPggNC5oAFggNC0YAPggNC1IAFggNC+oAPggNC2oAFQtWCA4APggNC14AF
|
||||
QryCA4APggNC5IAFQp6CA0KVgA6CA0L2gAaCA0K0gA5C9YICQv6ABoIDQs+ADkLqggOABoID
|
||||
QvSADkLdggOABkLhggOADkLXggOABkKzggJC74AOQs+CA4AHQscCQuWAD0LCggOAG4IDgBxC
|
||||
zULEgCBIi0jXSMaAHIgCSPaAGki3iANIq4ACSJBIu0jhSPFI1Ui9SIqAD0j4iASAAUjYiAhI
|
||||
3EitgAuIBYABiAxIy4AISIyIBYABiA6AB0jIiAWCA4AbggOAG4IDQpCAEUK7ggFCr4AEggNC
|
||||
wYAQQq2CA0KYgAOCA0LygBBC+YIDQuKAA4IEgBCCBELOgAOCBIAQggRCwoADggRCl4APggRC
|
||||
roADggRCq4APggRCk4ADggRCyoAPggSABIIEQu+AD4IEgARC14IEgA5CiIIEgARCqYIEgA5C
|
||||
p4IEgAWCBIAOQpSCBIAFggRCn4ANQoSCBIAFggRCy4ANQoWCBIAFQv6CA0LzgA6CBIAFQteC
|
||||
BIAOggSABUKsggSADoIEQn+ABYIEgA6CBEKIgAWCBEKbgA2CBEKTgAWCBIAOggRCvIAFQo2C
|
||||
AkLFgA6CBELZgAdCgoAQQsKCA0KZgBpC14IBQr2AH0joiAFIuIAaSNiIA0iKgBmIBYABSIVI
|
||||
74gESOhItUiBgA2IBYABiAtI0oAJSKSIBQBIxIgNSKWAB0jqiAUASL6IDkjEgAaIBgAAAAAA
|
||||
AAAAAAAAAYD/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+AA0iDAEjUyOwBSJJIr0jGSN1I7Ej8SOtIqYAISL9I
|
||||
9UjTAEjISPeABEjUSOdI7kikgANIm0jSSPJI24AISNNI90i5AEiYSNuAA0jTSOZI70ihgAVI
|
||||
nMjpAUiNgAZIkEjpSPBImYABSN6AAkjRSOpI70ijgAZIgEjbSOdIpYAGSK1I9kjggAJIuIAB
|
||||
SM5I7kjvSKmACEjVSPJIwIAGSOBI+UixgAJIlQBIy0jxSO5IroAJSL5I9EjXgAVIsUj2SNtI
|
||||
iIAESMlI7EiygApIukj4SNSABUjYSOtIxIAESJ5I9EjqSIWACUiQSNJI+0jDgARIo0jhSOpI
|
||||
n4AESMpI8ki5gAlImUjNSPpI0EicgARIvkj2SNaABUjESOtIv4AGSIJInkiuSNdI+UjaSKeA
|
||||
BEiESOdI9UikgAVIqEj2SOSAAkiSSJ9Iu0jYSOZI7UjlSPdI2UidgAVIvkj6SNSABkiRSO1I
|
||||
60iySMJI4EjqSOBI+Uj4SO5I3kjWSMxIhIAGSKBI2Ei0gAdI30j2SPhI90j5SOVI20jVSLdI
|
||||
nkiJgBRIwkjqSN1IykiwSIiAGkiGgP+A/4A+SMOIDUjUgAZI/ogFgAJIuIgFSMZI84gFgAaI
|
||||
A0jdiAGAAUi1iAWAA0jNiANIj4AESKWIA0igiAEASLGIBYAESICIA0jCgARI9ogDAEjbCEit
|
||||
iAWABogDSPKAA0iHiANI6gBIs4gGgAdI74gDgANIzIgDSJ6AAYgFgAhI3YgDgAOIBIACSNeI
|
||||
A4AISMKIA0j8gAJIwogDSLGAAkiFiAOABkitSNiIBEjDgAKIBIAEiAOAAUiXSMFI34gHSNOA
|
||||
AkiniANIy4AEiANI9ogKSOiAA0jliANIgIAEiA5I3IAESMSIAkjwgAWIDUi4gAZI3ghI9YAG
|
||||
iAhI10i9SJGAE0iSiANIyEiegBpIf4D/gP+AHogPgAVIgIgGgAKIDkiygARIxIgGgAGIBki9
|
||||
gAFIr4gESO2ABIgHAIgGSLuAA0jciASAA0iDiARIuYgISMCABEigiASAA0jkiAQAiAdIx4AG
|
||||
iARIooACiAUASLaIBUjOgAZIh4gESJWAAUi2iARIlIABiARI2YAGSJmIBYACiAWAAkjhiANI
|
||||
pYADSJ1I14gHgAFIpogESLGAA4gDSJ9ItEj3iApInYABiAWABIgQSLWAAogESNyABIgPSKGA
|
||||
A4gEgAWIDoAFSKaIAkjQgAWICkjwSLtIgIAISJyAB0jxiAVI00iVgBdIq0jbSK6A/4D/gBsA
|
||||
AP8AAP8AAP8AAf6A/4D/gFdCiIAdggJC8YAaggRCq4AYQrKCBELQgANC4kKKgBJC34IEQpmA
|
||||
A4IBQpCAEULCggNC7IAEggFC8IASggOABYICgBNCo0KrgAaCAoAcggKAHIICQp+AG4ICQsOA
|
||||
G4ICQt6AG4IDgP+A/4CSQrmCAUKVgBpCtIIDgBqCBIAEQoeAE0KFggSABAJC6oATggNCkIAE
|
||||
ggFClIASQrOCAULMgAWCAUKugByCAULOgByCAUL8gByCAoAcggKAHIICQoSAG4ICQrWA/4D/
|
||||
gLJChELTQsaAHELUQvtC7UKEgBpCjELxQvhCv4AFQoqAFULUQt1CiYAFQulCkYAdQuxCmIAd
|
||||
Qv1CtoAdAkLOgB1C9ELXgB1C8ULUgB1C9kLvQouAHELsQvtCqYD/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
HAAAAAAAAAAAAAAAAYIDgBuCA4AbggNCkYARQryCAUKwgASCA0LCgBBCroIDQpmAA4IDQvOA
|
||||
EEL6ggNC44ADggSAEIIEQs+AA4IEQoOAD4IEQsSAA4IEQvWAD4IEQq+AA4IFgA+CBEKUgAOC
|
||||
BELLgA+CBIAEggRC8IAPggSABELYggSADkKJggSABEKpggSADkKoggSABYIEgA5ClYIEgAWC
|
||||
BEKggA1ChYIEgAWCBELMgA1ChoIEgAWCBEL0gA6CBIAFQtiCBIAOggSABUKtggSADoIEQoCA
|
||||
BYIEgA6CBEKJgAWCBEKcgA2CBEKUgAWCBIAOggRCvYAFQo6CAkLGgA6CBELagAdCg4AQQsSC
|
||||
A0KagBpC2IIBQr6AH0jqiAFIuYAaSNmIA0iLgBmIBYABSIZI8IgESOlItkiCgA2IBYABiAtI
|
||||
1IAJSKWIBQBIxYgNSKaAB0jriAUASL+IDkjFgAaIBoICQtyAG4ICQveAG4IDgBNCuUK1gAWC
|
||||
A4ASggJC/YAEggNCloAQQpyCA0KIgAOCA0K2gBBCqYIDgASCBEKDgA9CsIIDgASCBEL1gA9C
|
||||
zoIDgASCBYAPQuWCA4AEggRCv4APQvSCAkL7gASCBEKTgA9C+YICQvSABYIDQq+AD4IDQuqA
|
||||
BYIDQtSAD4IDQteABYIDQv2AD4IDQt6ABULYggOAD4IDQtuABUK/ggOAD4IDQueABUKhggNC
|
||||
l4AOggNC+YAGggNCt4AOQvmCA4AGggNC0oAOQu2CA4AGggNC94AOQuCCA4AGQuSCA4AOQtqC
|
||||
A4AGQraCAkLygA5C0oIDgAdCygJC6IAPQsWCA0KAgBqCA4AcQtBCx4AgSI5I2kjKgBtIf4gC
|
||||
SPqAGki6iANIroACSJNIvkjlSPVI2UjASI2AD0j8iASAAUjbiAhI4EiwgAuIBYABiAxIzoAI
|
||||
SI+IBYABiA6AB0jLiAVC1EL4QruAHELMQu9Cv4AcQsRC/kLVgBxCr0L9QumAFEKnQqOABUKO
|
||||
Qu1C8EKVgBJCmULzQu5CjYAEQoJC30LnQqGAEkKcQutC3IAFggNC04ARQqNC80LjgAWCBEKX
|
||||
gBBCt0L+QuSABYIEQquAEELDAkLbgAWCBIARQsZC+ELJgAVCp0KaQu5C40KNgBFCwELyQsOA
|
||||
BkKHQuxC9UKegBFC1UL9QsOAB0LiAkK6gBFC4gJCu4AHQtECQs6AEULZQvxCvYAHQrtC/ULd
|
||||
gBFCx0LuQrWAB0KjQvJC5kJ/gBBC0kL6QsGAB0KXQulC6EKSgBBC0wJCzYAHQoVC6ELvQqCA
|
||||
EELLQv5Cz4AIQuBC+0K0gBBCxEL7QtGACELTAkLLgBBCvEL3QtGACELCAkLegBBCuUL5QtaA
|
||||
CEKYQtRCtIAQQrZC+ULbgBxCqkL3Qu5CiYAcQrVCrIBhSLtIq4AcSKxI+0jqSIeAG0jMSP0I
|
||||
SMmAA0iOSK5Iw0jLSLtIpEiRgBBI0EjqSPlI70iSgAJI5Ej8iAFI/UjkSPFI5kjVSLVImoAL
|
||||
SItI40j5SN4ISMGAAkjdSO1I3UjWSN1I3EjzSP4ISP1I70jcSKSACUisSPNI7UixSPNI9oC/
|
||||
QonChQGAHELtwuQBQt5CjIAaQvZC6ML0AUKggBpCn0KOQqxCqoD/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4DbAAAA
|
||||
AAAAAAAAAAABgAGID4AFSIGIBoACiA5ItIAESMWIBoABiAZIvoABSLCIBEjugASIBwCIBki8
|
||||
gANI3YgEgANIhIgESLqICEjBgARIoYgEgANI5YgEAIgHSMiABogESKOAAogFAEi3iAVIz4AG
|
||||
SIiIBEiWgAFIt4gESJWAAYgESNqABkiaiAVIgIABiAWAAkjiiANIpoADSJ9I2IgHgAFIp4gE
|
||||
SLKAA4gDSKBItUj4iApInoABiAWABIgQSLeAAogESN2ABIgPSKKAA4gEgAWIDoAFSKeIAkjR
|
||||
gAWICkjxSLxIgYAISJ2AB0jyiAVI1EiWgBdIrEjdSK+A/4D/gB1Ix4gNSNeABogGgAJIvIgF
|
||||
SMpI94gFgAaIA0jgiAGAAUi5iAWAA0jQiANIkoAESKiIA0ijiAEASLWIBYAESIOIA0jFgARI
|
||||
+ogDAEjfCEiwiAWABogDSPWAA0iKiANI7gBItogGgAdI84gDgANIz4gDSKKAAYgFgAhI4IgD
|
||||
gAOIBIACSNqIA0iAgAdIxYgEgAJIxogDSLSAAkiIiAOABkiwSNyIBEjHgAKIBIAEiAOAAUib
|
||||
SMVI44gHSNeAAkiqiANIzoAEiANI+ogKSOuAA0joiANIg4AEiA5I4IAESMeIAkjzgAWIDUi7
|
||||
gAZI4ghI+IAGiAhI2kjASJSAE0iWiANIy0ihgBpIg4D/gP+AIEiIAEjayPIBSJhItUjMSONI
|
||||
8ghI8UivgAhIxEj7SNkASM1I/YAESNpI7Uj0SKqAA0igSNdI+EjhgAhI2Uj9SL8ASJ1I4YAD
|
||||
SNhI7Ej1SKeABUihyO8BSJKABkiVSO9I9kiegAFI5IACSNZI8Ej1SKiABkiFSOFI7kirgAZI
|
||||
s0j8SOaAAki+gAFI1Ej0SPVIroAISNtI+EjGgAZI5ghItoACSJoASNFI90j0SLOACUjDSPpI
|
||||
3YAFSLZI/EjgSI2ABEjOSPJIuIAKSL9I/kjagAVI3kjxSMmABEikSPpI8EiKgAlIlUjXCEjJ
|
||||
gARIqEjnSPBIpIAESNBI+Ei/gAlInkjTCEjWSKGABEjDSPxI24AFSMpI8kjEgAZIh0ijSLNI
|
||||
3QhI30itgARIiUjtSPtIqYAFSK5I/EjqgAJIl0ilSMFI3UjsSPNI60j9SN9IooAFSMQISNqA
|
||||
BkiWSPNI8Ui4SMhI5kjwSOYISP5I9EjkSNxI0kiKgAZIpUjeSLmAB0jlSPxI/kj9CEjrSOFI
|
||||
20i8SKRIjoAUSMdI8EjjSNBItkiNSIGAGUiLgP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+APAD//wD/
|
||||
/wD//wH//4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+AZ0jJSNhIi4AbSIxI7Uj0SKyAG0iSSOVI
|
||||
30jwSLWAGkibSOtI4EjuSOtIr4ASSJqABUilSPBI3EjOSPFI5kiogP+A/4D/gFhIoogBSM6A
|
||||
G4gDSJqAGUiJiARIgYASSKyABEiciAWAEgiABEiqiAVI+4ARCEjegANIuIgGSNuA/4D/gP+A
|
||||
OEiYSKaAHIgDSIOAGUi8iANI+oATSJ6ABEjmiARI3YASCIAESPuIBUjNgBEISL6AA4gHSLyA
|
||||
EIgBgAOICEiDgA8AAAAAAAAAAAAAAAGA/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/SNyABUixSPNI1QBI00j0
|
||||
SOBIhoAQSOJIl4AESLxI9UjMAEiISOZI7EjPgBBI8kimgARIz0jtSMCAAUisSO5I80i2gA9I
|
||||
+kjagARI2UjuSKyAAkjFSOtI7UingA5I6kjzSJ2AAkiUSOhI60iRgANI00j4SN9In4ANSMpI
|
||||
+0jKgAJIvEj7SNiABEiNSNFI/EjjSJuADEinSPJI6kiGgAFI3Ej3SLCABUibSNtI8kjpSKCA
|
||||
DEjPSO9IvwBIpkjpSNtIl4AGSI1I20jxSOdIqIALSL9I8EjOAEi8SPFI1YAISIZI2Uj1SLaA
|
||||
C0iFSOpI80i+SOdI9kilgAlIhUikgA1IuUj4SOVI8EjdgBtI3EjwSORI0YAbSIpI3kj8SMyA
|
||||
HEifSM5IoYD/gP+AWIgBgANIz4gHSLKAD4gBSKqAAkjliAhIiYAOiAFI4oACiANI+kjtiASA
|
||||
DogCgAKIA0jKAIgESO+ADYgCSMAASLWIA0iVAEibiARI7IAMiAMASN+IA4ACSLqIBEj4gAuI
|
||||
A0iTiANI4YADSM2IBEj7gAqIA0jxiANIsIAESNWIBEjKgAlI8IgHgAZIz4gDSNiACUiTiAZI
|
||||
woAHSMyIAkiKgAqIBkiDgAhIk0iygAxIlogFgBlI1IgEgBpI2YgCSNKAG0jRCEjTgP+A/4A4
|
||||
iAFIqoACiAmAD4gCgAKICUjmgA6IAoABSJmICki3gA2IAkjKAEjViARIzIgFSKOADIgDAIgE
|
||||
SPMASPuIBUiogAuIA0ihiARIpIABiAZIr4AKiANI8YgEgAJIgogGSL2ACYgJgANIm4gGgAmI
|
||||
CEihgARIiIgFgAlI8ogHgAeIA0jngAlIoIgGSOCACEjyCEjUgAtI9YgFSLCAGIgFgBlIhIgE
|
||||
gBpIoogCSKeA/4D/gDcAAP8AAP8AAP8AAf+A/4D/gP+AJ0iaSKiAHIgDSISAGUi9iANI+4AT
|
||||
SJ+ABEjniARI3oASCIAESP2IBUjOgBEISL+AA4gHSL2AEIgBgAOICEiEgP+A/4D/gFZIpYgB
|
||||
SNGAG4gDSJ2AGUiMiARIhIASSLCABEifiAWAEgiABEitiAaAEQhI4YADSLuIBkjegP+A/4D/
|
||||
gHhIz0jeSJCAG0iRSPNI+kixgBtIl0jrSOVI9ki6gBpIoUjxSOVI80jxSLSAEkifgAVIqkj2
|
||||
SOJI1Ej3SOxIrYD/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/
|
||||
gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+AEQAAAAAAAAAAAAAAAYgBSKuAAogJgA+IAoAC
|
||||
iAlI54AOiAKAAUiaiApIuIANiAJIzABI1ogESM2IBUikgAyIAwCIBEj1AEj8iAVIqYALiANI
|
||||
oogESKWAAYgGSLCACogDSPKIBIACSIOIBki+gAmICYADSJyIBoAJiAhIooAESImIBYAJSPOI
|
||||
B4AHiANI6YAJSKGIBkjhgAhI8whI1YALSPaIBUiygBiIBYAZSIWIBIAaSKOIAkipgP+A/4A3
|
||||
iAGAA0jSiAdItoAPiAFIrYACSOiICEiMgA6IAUjlgAKIA0j+SPCIBIAOiAKAAogDSM0AiARI
|
||||
84ANiAJIwwBIuIgDSJgASJ6IBEjwgAyIAwBI4ogDgAJIvogESPuAC4gDSJaIA0jlgANI0IgF
|
||||
gAqIA0j1iANIs4AESNiIBEjNgAlI9IgHgAZI0ogDSNuACUiXiAZIxYAHSM+IAkiNgAqIBkiG
|
||||
gAhIlki1gAxImYgFgBlI14gEgBpI3YgCSNWAG0jUCEjXgP+A/4A4SOGABUi2SPlI2gBI2Ej6
|
||||
SOVIi4AQSOhInYAESMJI+0jSAEiNSOtI8kjVgBBI90irgARI1UjzSMWAAUixSPRI+Ui8gA8I
|
||||
SN+AA0iASN5I9EiygAJIy0jxSPNIrIAOSPBI+UiigAJImUjuSPFIloADSNlI/kjlSKSADUjP
|
||||
CEjQgAJIwQhI3oAESJJI1whI6UihgAxIrUj4SPBIi4ABSOJI/Ui2gAVIoEjhSPhI70ilgAtI
|
||||
gUjVSPVIxQBIq0juSOFInIAGSJJI4Uj2SO1IroALSMVI9kjTSIFIwUj3SNqACEiMSN5I+ki7
|
||||
gAtIikjvSPlIw0jsSPxIqoAJSIpIqYANSL5I/kjrSPZI44AbSOFI9kjpSNeAG0iPSOQISNKA
|
||||
HEikSNNIpoD/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A
|
||||
/4D/gP+A/4D/gP+A/4D/gP+A/4D/gP+A/4D/gFg=]]></BinaryString>
|
||||
<BinaryString name="Tags"></BinaryString>
|
||||
<float name="TopParamA">-0.5</float>
|
||||
<float name="TopParamB">0.5</float>
|
||||
<token name="TopSurface">3</token>
|
||||
<token name="TopSurfaceInput">0</token>
|
||||
<float name="Transparency">0</float>
|
||||
<Vector3 name="Velocity">
|
||||
<X>0</X>
|
||||
<Y>0</Y>
|
||||
<Z>0</Z>
|
||||
</Vector3>
|
||||
<Color3 name="WaterColor">
|
||||
<R>0.0470588244</R>
|
||||
<G>0.329411775</G>
|
||||
<B>0.360784322</B>
|
||||
</Color3>
|
||||
<float name="WaterReflectance">1</float>
|
||||
<float name="WaterTransparency">0.300000012</float>
|
||||
<float name="WaterWaveSize">0.150000006</float>
|
||||
<float name="WaterWaveSpeed">10</float>
|
||||
<Vector3 name="size">
|
||||
<X>2044</X>
|
||||
<Y>252</Y>
|
||||
<Z>2044</Z>
|
||||
</Vector3>
|
||||
</Properties>
|
||||
</Item>
|
||||
</roblox>
|
||||
13
test-projects/terrain/default.project.json
Normal file
13
test-projects/terrain/default.project.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "terrain",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"Workspace": {
|
||||
"$className": "Workspace",
|
||||
|
||||
"Terrain": {
|
||||
"$path": "Terrain.rbxmx"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user