Compare commits

..

60 Commits

Author SHA1 Message Date
Lucien Greathouse
420627d892 0.5.0-alpha.11 2019-05-29 14:07:15 -07:00
Lucien Greathouse
ce3a409997 Undo 0.5.0-alpha.10 release due to regression 2019-05-29 13:38:36 -07:00
Lucien Greathouse
0f9f1782ae 0.5.0-alpha.10 2019-05-29 13:24:06 -07:00
Lucien Greathouse
d4704a02c5 Upgrade dependencies 2019-05-29 13:15:22 -07:00
Lucien Greathouse
9ca2ed2c93 plugin: upgrade Roact 2019-05-16 18:45:35 -07:00
Lucien Greathouse
ae12ffdefb Work around Roact bug 2019-05-16 18:45:00 -07:00
Lucien Greathouse
1e13097126 plugin: update rbx-dom 2019-05-16 18:33:37 -07:00
Lucien Greathouse
9b8a6b1168 Add terrain test project 2019-05-16 18:03:11 -07:00
Lucien Greathouse
8f6dda5cd3 Use rbx_xml 0.9.0's config to read unknown properties 2019-05-16 17:58:32 -07:00
Lucien Greathouse
91780f236e Update dependencies 2019-05-16 17:58:19 -07:00
Lucien Greathouse
f16474815c plugin: update rbx-dom 2019-05-15 11:19:05 -07:00
Lucien Greathouse
a8ff6d7e6e Update dependencies 2019-05-14 18:23:01 -07:00
Lucien Greathouse
8395782a2e Use Display instead of Debug for rbx_xml errors now 2019-05-14 17:55:18 -07:00
Lucien Greathouse
28ea625b01 Plugin: Port reconciler to use rbx_dom_lua 2019-05-14 14:22:55 -07:00
Lucien Greathouse
efc569f6ed Plugin: Update rbx-dom 2019-05-14 14:22:44 -07:00
Lucien Greathouse
d377e10771 Update rbx-dom 2019-05-13 17:35:55 -07:00
Lucien Greathouse
fef85877e6 Add safeguards against accidentally committing model or place files 2019-05-13 17:29:41 -07:00
Lucien Greathouse
19135bfaf4 Add RbxDom library as piece of plugin 2019-05-13 17:29:41 -07:00
Lucien Greathouse
5a147fccc2 Add rbx-dom as Git submodule to plugin 2019-05-13 17:29:41 -07:00
Lucien Greathouse
20976814ba Upgrade a bunch of small dependencies 2019-05-12 12:57:59 -07:00
Lucien Greathouse
27e2612fc9 Upgrade rbx_dom_weak, rbx_reflection, and rbx_xml 2019-05-12 12:57:24 -07:00
Lucien Greathouse
3ea432ef2d Fix up docs on model/place files a little 2019-05-09 13:29:03 -07:00
Lucien Greathouse
fe6acbc1e3 Clean up repo cruft 2019-05-04 21:01:10 -07:00
Lucien Greathouse
379b162e64 Fix dependency paths changing.
Roact 1.0 changed from lib to src!
t changed from lib/t.lua to lib/init.lua, so we just use lib
2019-05-04 19:33:08 -07:00
Lucien Greathouse
84832955dd Upgrade to Roact 1.0 and latest t 2019-05-04 00:05:45 -07:00
Lucien Greathouse
34b99a51c3 Relax debug assert in IMFS, since paths can alias now 2019-04-30 23:06:59 -07:00
Lucien Greathouse
fb5245e2af Update dependencies 2019-04-22 18:26:28 -07:00
Diego Alpízar
ff0a830e0c Minor typo fix (#156)
Fix repeated "available available"
2019-04-06 23:38:29 -07:00
eryn L. K
a365f071a4 Update installation.md (#155) 2019-04-05 17:20:53 -07:00
Lucien Greathouse
f290e7b5b2 Support implicit values in JSON models (#154)
* Support implicit values in JSON models

* Update Changelog
2019-04-05 15:17:58 -07:00
Lucien Greathouse
83a0ae673c 0.5.0-alpha.9 2019-04-04 21:20:00 -07:00
Lucien Greathouse
7de646c290 Upgrade dependencies 2019-04-04 18:35:18 -07:00
Lucien Greathouse
5d681a72ac Rewrite CSV conversion to dodge Serde (#152)
* Rewrite CSV conversion to dodge Serde

* Update CHANGELOG
2019-04-04 18:21:55 -07:00
Lucien Greathouse
d725970e6e Fix handling of CSV files with empty columns and rows (#149)
* Fix #147

* Add localization test project, fix empty rows in general

* Fill out 'normal' CSV in localization test project

* Update Changelog
2019-04-04 13:16:10 -07:00
Lucien Greathouse
54b82760cd Switch 'rojo build' to use BufWriter, magic performance increase 2019-04-01 18:02:46 -07:00
Lucien Greathouse
77f79fa913 0.5.0-alpha.8 2019-03-29 17:36:43 -07:00
Lucien Greathouse
6db714a2b1 Special-case Lighting.Technology in setCanonicalProperty, temporary fix 2019-03-29 17:25:57 -07:00
Lucien Greathouse
913ac7c9f5 Update dependencies 2019-03-28 15:44:56 -07:00
Lucien Greathouse
eecbfd29e7 Update dependencies, adding a bunch of new features 2019-03-27 13:31:12 -07:00
Lucien Greathouse
41025225b2 Rewrite message queue with oneshot futures (#139) 2019-03-27 13:27:50 -07:00
Lucien Greathouse
07c7b28c03 Fix plugin unloading 2019-03-21 22:35:30 -07:00
Lucien Greathouse
3faf3d2a56 Update Changelog for #135 2019-03-20 10:42:18 -07:00
Lucien Greathouse
be094d5b7c Make snapshot application communicative (#135)
* Add children sorting to snapshot_reconciler

* Update snapshot tests to include stable children order

* Bump dependencies, which should make this PR work
2019-03-20 10:39:53 -07:00
Lucien Greathouse
459673bd59 0.5.0-alpha.6 2019-03-19 18:24:30 -07:00
Lucien Greathouse
2968b70e6b Listen to Plugin.Unloading.
Closes #127.
2019-03-19 18:17:03 -07:00
Lucien Greathouse
b6989a18fc Add conditionally-enabled typechecking using t 2019-03-19 17:57:19 -07:00
Lucien Greathouse
4d6a504836 Remove Rodux and Roact-Rodux, add t dependency 2019-03-19 16:34:53 -07:00
Lucien Greathouse
6c3737df68 Update Changelog 2019-03-19 16:31:34 -07:00
Lucien Greathouse
9f382ed9bd Iterate on plugin reconciler
- Renamed setProperty to setCanonicalProperty, which is more usefully
  descriptive. Also added a detailed comment.
- Fixed reconciler behavior with regards to removing known instances
  when $ignoreUnknownInstances is set
2019-03-19 16:30:06 -07:00
Lucien Greathouse
f9e86e58d6 Add InstanceMap:destroyInstance for forgetting and destroying in one step 2019-03-19 16:29:56 -07:00
Lucien Greathouse
469f9c927f Improve plugin place project for testing 2019-03-19 16:29:31 -07:00
Lucien Greathouse
312724189b Remove ignore from old doc generator script 2019-03-14 14:20:38 -07:00
Lucien Greathouse
ec0a1f1ce4 New snapshot tests (#134)
* Changes project-related structures to use `BTreeMap` instead of `HashMap` for children to aid determiniusm
* Changes imfs-related structures to have total ordering and use `BTreeSet` instead of `HashSet`
* Upgrades dependencies to `bx_dom_weak`1.2.0 and rbx_xml 0.5.0 to aid in more determinism stuff
* Re-exposes the `RbxSession`'s root project via `root_project()`
* Implements `Default` for a couple things
* Tweaks visualization code to support visualizing trees not attached to an `RbxSession`
* Adds an ID-invariant comparison method for `rbx_tree` relying on previous determinism changes
* Adds a (disabled) test to start finding issues in the reconciler with regards to communicativity of snapshot application
* Adds a snapshot testing system that operates on `RbxTree` and associated metadata, which are committed in this change
2019-03-14 14:20:03 -07:00
Lucien Greathouse
ad93631ef8 Port to futures channel instead of std one.
Fixes #133.
2019-03-12 11:45:39 -07:00
Lucien Greathouse
3b6238ff93 Add more types to plugin 2019-03-11 16:55:42 -07:00
Lucien Greathouse
5b9facee00 Fix up variable naming in serialize_unresolved_minimal 2019-03-11 16:35:54 -07:00
Lucien Greathouse
376f2a554a Better default project, including minimal property types 2019-03-11 16:28:40 -07:00
Lucien Greathouse
5fd0bd3db9 Update/prune dependencies with help of cargo-outdated 2019-03-11 14:12:49 -07:00
Lucien Greathouse
2deb3bbf23 Add notable feature from dependency upgrade 2019-03-11 13:48:02 -07:00
Lucien Greathouse
01bef0c2b8 Update dependencies 2019-03-11 13:47:33 -07:00
78 changed files with 3934 additions and 1066 deletions

6
.gitignore vendored
View File

@@ -2,4 +2,8 @@
/target
/scratch-project
**/*.rs.bk
/generate-docs.run
/server/failed-snapshots/
/*.rbxm
/*.rbxmx
/*.rbxl
/*.rbxlx

12
.gitmodules vendored
View File

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

View File

@@ -2,6 +2,52 @@
## [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

1026
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
[workspace]
members = [
"server",
"rojo-e2e",
]
]
[profile.dev]
opt-level = 1

View File

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

View File

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

View File

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

View File

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

View File

@@ -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/t Submodule

Submodule plugin/modules/t added at f643b50682

View File

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

View File

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

View File

@@ -80,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 = {
@@ -182,6 +183,13 @@ function App:didMount()
preloadAssets()
end
function App:willUnmount()
if self.currentSession ~= nil then
self.currentSession:disconnect()
self.currentSession = nil
end
end
function App:didUpdate()
local connectActive = self.state.sessionStatus == SessionStatus.ConfiguringSession
or self.state.sessionStatus == SessionStatus.Connected

View File

@@ -1,6 +1,6 @@
return {
codename = "Epiphany",
version = {0, 5, 0, "-alpha.5"},
version = {0, 5, 0, "-alpha.11"},
expectedServerVersionString = "0.5.0 or newer",
protocolVersion = 2,
defaultHost = "localhost",

View File

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

View File

@@ -45,6 +45,16 @@ function InstanceMap:removeInstance(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)

View File

@@ -1,7 +1,16 @@
local t = require(script.Parent.Parent.t)
local InstanceMap = require(script.Parent.InstanceMap)
local Logging = require(script.Parent.Logging)
local setProperty = require(script.Parent.setProperty)
local setCanonicalProperty = require(script.Parent.setCanonicalProperty)
local rojoValueToRobloxValue = require(script.Parent.rojoValueToRobloxValue)
local Types = require(script.Parent.Types)
local function setParent(instance, newParent)
pcall(function()
instance.Parent = newParent
end)
end
local Reconciler = {}
Reconciler.__index = Reconciler
@@ -24,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
@@ -43,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, rojoValueToRobloxValue(value))
setCanonicalProperty(instance, key, rojoValueToRobloxValue(value))
end
local existingChildren = instance:GetChildren()
@@ -81,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
@@ -100,13 +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)
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
@@ -114,28 +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
setProperty(instance, key, rojoValueToRobloxValue(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

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

View File

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

View File

@@ -1,32 +1,33 @@
local primitiveTypes = {
String = true,
Bool = true,
Int32 = true,
Float32 = true,
Enum = true,
}
local directConstructors = {
CFrame = CFrame.new,
Color3 = Color3.new,
Vector2 = Vector2.new,
Vector2int16 = Vector2int16.new,
Vector3 = Vector3.new,
Vector3int16 = Vector3int16.new,
}
local RbxDom = require(script:FindFirstAncestor("Rojo").RbxDom)
local function rojoValueToRobloxValue(value)
if primitiveTypes[value.Type] then
return value.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
local constructor = directConstructors[value.Type]
if constructor ~= nil then
return constructor(unpack(value.Value))
-- 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 errorMessage = ("The Rojo plugin doesn't know how to handle values of type %q yet!"):format(tostring(value.Type))
error(errorMessage)
local success, decodedValue = RbxDom.EncodedValue.decode(value)
if not success then
error(decodedValue, 2)
end
return decodedValue
end
return rojoValueToRobloxValue

View File

@@ -1,21 +1,17 @@
local RbxDom = require(script:FindFirstAncestor("Rojo").RbxDom)
local Logging = require(script.Parent.Logging)
--[[
Attempts to set a property on the given instance, correctly handling
'virtual properties', which aren't reflected directly to Lua.
Attempts to set a property on the given instance.
]]
local function setProperty(instance, key, value)
-- The 'Contents' property of LocalizationTable isn't directly exposed, but
-- has corresponding (deprecated) getters and setters.
if instance.ClassName == "LocalizationTable" and key == "Contents" then
instance:SetContents(value)
return
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 = pcall(function()
return instance[key]
end)
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
@@ -23,17 +19,13 @@ local function setProperty(instance, key, value)
-- 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
return false
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)
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)
@@ -42,4 +34,4 @@ local function setProperty(instance, key, value)
return true
end
return setProperty
return setCanonicalProperty

View File

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

View File

@@ -1,6 +0,0 @@
[package]
name = "rojo-e2e"
version = "0.1.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
[dependencies]

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "rojo"
version = "0.5.0-alpha.5"
version = "0.5.0-alpha.11"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "A tool to create robust Roblox projects"
license = "MIT"
@@ -29,17 +29,15 @@ hyper = "0.12"
log = "0.4"
maplit = "1.0.1"
notify = "4.0"
rand = "0.4"
rbx_binary = "0.4.0"
rbx_dom_weak = "1.0.0"
rbx_xml = "0.4.0"
rbx_reflection = "2.0.374"
rbx_dom_weak = "1.7.0"
rbx_xml = "0.9.0"
rbx_reflection = "3.0.384"
regex = "1.0"
reqwest = "0.9.5"
rlua = "0.16"
ritz = "0.1.0"
serde = "1.0"
serde_derive = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "0.7", features = ["v4", "serde"] }
@@ -47,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"

View 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
]
}
}
}
}
}

View File

@@ -1,7 +1,7 @@
use std::{
path::PathBuf,
fs::File,
io,
io::{self, Write, BufWriter},
};
use log::info;
@@ -74,6 +74,11 @@ impl_from!(BuildError {
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))
@@ -92,7 +97,7 @@ 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 mut file = BufWriter::new(File::create(&options.output_file)?);
match output_kind {
OutputKind::Rbxmx => {
@@ -100,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
@@ -108,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();
@@ -121,5 +126,7 @@ pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
},
}
file.flush()?;
Ok(())
}

View File

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

View File

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

View File

@@ -85,6 +85,10 @@ impl LiveSession {
Ok(())
}
pub fn root_project(&self) -> &Project {
&self.project
}
pub fn session_id(&self) -> SessionId {
self.session_id
}

View File

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

View File

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

View File

@@ -10,7 +10,7 @@
//!
//! ```
//! # use std::path::PathBuf;
//! # use serde_derive::{Serialize, Deserialize};
//! # use serde::{Serialize, Deserialize};
//!
//! #[derive(Serialize, Deserialize)]
//! struct Mine {

View File

@@ -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_dom_weak::{UnresolvedRbxValue, RbxValue};
use serde_derive::{Serialize, Deserialize};
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";
@@ -55,15 +56,88 @@ 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")]
#[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")]
@@ -73,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
@@ -162,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 {
@@ -170,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),
}
}
}
@@ -187,7 +263,7 @@ pub enum ProjectSaveError {
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ProjectNode {
pub class_name: Option<String>,
pub children: HashMap<String, ProjectNode>,
pub children: BTreeMap<String, ProjectNode>,
pub properties: HashMap<String, UnresolvedRbxValue>,
pub ignore_unknown_instances: Option<bool>,
@@ -259,47 +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,
}.into(),
},
..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,
plugins: Vec::new(),
serve_port: None,
serve_place_ids: None,
file_location: project_path.clone(),
};
project.name = project_name.to_owned();
project.save()
.map_err(ProjectInitError::SaveError)?;
@@ -387,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)?;
@@ -434,6 +485,10 @@ impl Project {
}
}
pub fn folder_location(&self) -> &Path {
self.file_location.parent().unwrap()
}
fn to_source_project(&self) -> SourceProject {
let plugins = self.plugins
.iter()

View File

@@ -7,7 +7,7 @@ use std::{
};
use rlua::Lua;
use serde_derive::{Serialize, Deserialize};
use serde::{Serialize, Deserialize};
use log::{info, trace, error};
use rbx_dom_weak::{RbxTree, RbxId};
@@ -251,6 +251,10 @@ 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)
}

View File

@@ -13,8 +13,8 @@ use rlua::Lua;
use failure::Fail;
use log::info;
use maplit::hashmap;
use rbx_dom_weak::{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::{
@@ -106,6 +106,7 @@ pub enum SnapshotError {
},
XmlModelDecodeError {
#[fail(cause)]
inner: rbx_xml::DecodeError,
path: PathBuf,
},
@@ -115,6 +116,12 @@ pub enum SnapshotError {
path: PathBuf,
},
CsvDecodeError {
#[fail(cause)]
inner: csv::Error,
path: PathBuf,
},
ProjectNodeUnusable,
ProjectNodeInvalidTransmute {
@@ -146,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.")
},
@@ -475,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");
@@ -506,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> {
@@ -554,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))
@@ -570,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(),
}
})
}
}
@@ -597,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() {

View File

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

View File

@@ -11,7 +11,7 @@ use std::{
};
use rbx_dom_weak::{RbxTree, RbxId, RbxInstanceProperties, RbxValue};
use serde_derive::{Serialize, Deserialize};
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, Clone, 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>,
@@ -153,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);
@@ -164,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(
@@ -222,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;
@@ -279,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,
@@ -287,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
@@ -303,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;
}
@@ -311,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;
}
@@ -319,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() {
@@ -348,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());
}

View File

@@ -1,4 +1,5 @@
use std::{
collections::HashMap,
fmt,
io::Write,
path::Path,
@@ -6,12 +7,13 @@ use std::{
};
use log::warn;
use rbx_dom_weak::RbxId;
use rbx_dom_weak::{RbxTree, RbxId};
use crate::{
imfs::{Imfs, ImfsItem},
rbx_session::RbxSession,
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("\"", "&quot;")
.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(())

View File

@@ -4,10 +4,14 @@
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
sync::{mpsc, Arc},
sync::Arc,
};
use futures::{future, Future};
use futures::{
future::{self, IntoFuture},
Future,
sync::oneshot,
};
use hyper::{
service::Service,
header,
@@ -17,7 +21,7 @@ use hyper::{
Request,
Response,
};
use serde_derive::{Serialize, Deserialize};
use serde::{Serialize, Deserialize};
use rbx_dom_weak::{RbxId, RbxInstance};
use crate::{
@@ -114,14 +118,16 @@ impl Service for ApiService {
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/subscribe/") => self.handle_api_subscribe(request),
(&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))
@@ -152,57 +158,41 @@ impl ApiService {
/// 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, request: Request<Body>) -> Response<Body> {
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 Response::builder()
return Box::new(future::ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.header(header::CONTENT_TYPE, "text/plain")
.body(Body::from(err.to_string()))
.unwrap();
.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,
})
}
}
// TOOD: Switch to futures mpsc instead to not block this task
let (tx, rx) = mpsc::channel();
let sender_id = message_queue.subscribe(tx);
match rx.recv() {
Ok(_) => (),
Err(_) => return Response::builder()
.status(500)
.body(Body::from("error!"))
.unwrap(),
}
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, request: Request<Body>) -> Response<Body> {

View File

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

View File

@@ -1,7 +1,7 @@
#[macro_use] extern crate lazy_static;
use std::{
collections::HashMap,
collections::{HashMap, BTreeMap},
path::{Path, PathBuf},
};
@@ -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 {
@@ -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);

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

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

View File

@@ -1,128 +0,0 @@
use std::{
fs::{self, File},
path::{Path, PathBuf},
};
use pretty_assertions::assert_eq;
use librojo::{
imfs::Imfs,
project::{Project, ProjectNode},
rbx_snapshot::{SnapshotContext, 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 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),
}
}
/// 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");
}

View 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(())
}
}

View 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");
}

View 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(())
}

View 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)
}

View File

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

View 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"
}
]
}
}

View File

@@ -0,0 +1,10 @@
{
"Name": "children",
"ClassName": "Folder",
"Children": [
{
"Name": "The Child",
"ClassName": "StringValue"
}
]
}

View File

@@ -0,0 +1,11 @@
{
"Name": "explicit",
"ClassName": "StringValue",
"Properties": {
"Value": {
"Type": "String",
"Value": "Hello, world!"
}
},
"Children": []
}

View File

@@ -0,0 +1,7 @@
{
"Name": "implicit",
"ClassName": "StringValue",
"Properties": {
"Value": "What's happenin', Earth?"
}
}

View File

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

View 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"
}
]
}
}

View 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,,
1 Key Source Context Example
2
3 Metadata Language.Name English
4 Language.Region United States
5
6 Options Label.Thickness Thickness
7 Label.Opacity Opacity
8
9 Toolbar Toolbar.Undo Undo
10 Toolbar.Redo Redo
11
12 Toolbar.Camera Top-down camera
13 Toolbar.Saves Saved drawings
14 Toolbar.Preferences Settings
15
16 Toolbar.Mode.Vector Vector mode
17 Toolbar.Mode.Pixel Pixel mode

View File

@@ -0,0 +1,2 @@
Key,Source,Context,Example,es
Count,3,,A number demonstrating issue 145,7
1 Key Source Context Example es
2 Count 3 A number demonstrating issue 145 7

View File

@@ -0,0 +1,2 @@
Key,Source,Context,Example,es
Ack,Ack!,,An exclamation of despair,¡Ay!
1 Key Source Context Example es
2 Ack Ack! An exclamation of despair ¡Ay!

View File

@@ -0,0 +1 @@
Hello world, from a/foo.txt

View File

@@ -0,0 +1 @@
-- hello, from a/main.lua

View File

@@ -0,0 +1 @@
-- b/something.lua

View 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
}
}
}
}

View 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
}
]
}
}

View 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
}
]
}
}
}

View 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"
}
]
}
}
}

View 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
}
]
}
}
}

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

View File

@@ -0,0 +1,13 @@
{
"name": "terrain",
"tree": {
"$className": "DataModel",
"Workspace": {
"$className": "Workspace",
"Terrain": {
"$path": "Terrain.rbxmx"
}
}
}
}