mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-23 14:15:24 +00:00
Compare commits
27 Commits
v0.5.0-alp
...
v0.5.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b65a8ce680 | ||
|
|
5fc4f63238 | ||
|
|
9b0e0c175b | ||
|
|
eb97e925e6 | ||
|
|
16f8975b18 | ||
|
|
5073fce2f7 | ||
|
|
cf5036eec6 | ||
|
|
20be37dd8b | ||
|
|
93349ae2dc | ||
|
|
be81de74cd | ||
|
|
88e739090d | ||
|
|
7f324f1957 | ||
|
|
4f31c9e72f | ||
|
|
c9a663ed39 | ||
|
|
105d8aeb6b | ||
|
|
6ea1211bc5 | ||
|
|
c13291a598 | ||
|
|
aaa78c618c | ||
|
|
2890c677d4 | ||
|
|
51a010de00 | ||
|
|
ca0aabd814 | ||
|
|
91d1ba1910 | ||
|
|
c7c739dc00 | ||
|
|
7a8389bf11 | ||
|
|
5f062b8ea3 | ||
|
|
b9ee14a0f9 | ||
|
|
c3baf73455 |
49
.travis.yml
49
.travis.yml
@@ -1,36 +1,41 @@
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- language: python
|
# Lua tests are currently disabled because of holes in Lemur that are pretty
|
||||||
env:
|
# tedious to fix. It should be fixed by either adding missing features to
|
||||||
- LUA="lua=5.1"
|
# Lemur or by migrating to a CI system based on real Roblox instead.
|
||||||
|
|
||||||
before_install:
|
# - language: python
|
||||||
- pip install hererocks
|
# env:
|
||||||
- hererocks lua_install -r^ --$LUA
|
# - LUA="lua=5.1"
|
||||||
- export PATH=$PATH:$PWD/lua_install/bin
|
|
||||||
|
|
||||||
install:
|
# before_install:
|
||||||
- luarocks install luafilesystem
|
# - pip install hererocks
|
||||||
- luarocks install busted
|
# - hererocks lua_install -r^ --$LUA
|
||||||
- luarocks install luacov
|
# - export PATH=$PATH:$PWD/lua_install/bin
|
||||||
- luarocks install luacov-coveralls
|
|
||||||
- luarocks install luacheck
|
|
||||||
|
|
||||||
script:
|
# install:
|
||||||
- cd plugin
|
# - luarocks install luafilesystem
|
||||||
- luacheck src
|
# - luarocks install busted
|
||||||
- lua -lluacov spec.lua
|
# - luarocks install luacov
|
||||||
|
# - luarocks install luacov-coveralls
|
||||||
|
# - luarocks install luacheck
|
||||||
|
|
||||||
after_success:
|
# script:
|
||||||
- cd plugin
|
# - cd plugin
|
||||||
- luacov-coveralls -e $TRAVIS_BUILD_DIR/lua_install
|
# - luacheck src
|
||||||
|
# - lua -lluacov spec.lua
|
||||||
|
|
||||||
|
# after_success:
|
||||||
|
# - cd plugin
|
||||||
|
# - luacov-coveralls -e $TRAVIS_BUILD_DIR/lua_install
|
||||||
|
|
||||||
- language: rust
|
- language: rust
|
||||||
rust: 1.31.1
|
rust: 1.32.0
|
||||||
cache: cargo
|
cache: cargo
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- cargo test --verbose
|
- cargo test --verbose
|
||||||
|
- cargo test --verbose --all-features
|
||||||
|
|
||||||
- language: rust
|
- language: rust
|
||||||
rust: stable
|
rust: stable
|
||||||
@@ -38,6 +43,7 @@ matrix:
|
|||||||
|
|
||||||
script:
|
script:
|
||||||
- cargo test --verbose
|
- cargo test --verbose
|
||||||
|
- cargo test --verbose --all-features
|
||||||
|
|
||||||
- language: rust
|
- language: rust
|
||||||
rust: beta
|
rust: beta
|
||||||
@@ -45,3 +51,4 @@ matrix:
|
|||||||
|
|
||||||
script:
|
script:
|
||||||
- cargo test --verbose
|
- cargo test --verbose
|
||||||
|
- cargo test --verbose --all-features
|
||||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -2,6 +2,18 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.5.0 Alpha 5](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.5) (March 1, 2019)
|
||||||
|
* Upgraded core dependencies, which improves compatibility for lots of instance types
|
||||||
|
* Upgraded from `rbx_tree` 0.2.0 to `rbx_dom_weak` 1.0.0
|
||||||
|
* Upgraded from `rbx_xml` 0.2.0 to `rbx_xml` 0.4.0
|
||||||
|
* Upgraded from `rbx_binary` 0.2.0 to `rbx_binary` 0.4.0
|
||||||
|
* Added support for non-primitive types in the Rojo plugin.
|
||||||
|
* Types like `Color3` and `CFrame` can now be updated live!
|
||||||
|
* Fixed plugin assets flashing in on first load ([#121](https://github.com/LPGhatguy/rojo/issues/121))
|
||||||
|
* Changed Rojo's HTTP server from Rouille to Hyper, which reduced the release size by around a megabyte.
|
||||||
|
* Added property type inference to projects, which makes specifying services a lot easier ([#130](https://github.com/LPGhatguy/rojo/pull/130))
|
||||||
|
* Made error messages from invalid and missing files more user-friendly
|
||||||
|
|
||||||
## [0.5.0 Alpha 4](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.4) (February 8, 2019)
|
## [0.5.0 Alpha 4](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.4) (February 8, 2019)
|
||||||
* Added support for nested partitions ([#102](https://github.com/LPGhatguy/rojo/issues/102))
|
* Added support for nested partitions ([#102](https://github.com/LPGhatguy/rojo/issues/102))
|
||||||
* Added support for 'transmuting' partitions ([#112](https://github.com/LPGhatguy/rojo/issues/112))
|
* Added support for 'transmuting' partitions ([#112](https://github.com/LPGhatguy/rojo/issues/112))
|
||||||
|
|||||||
841
Cargo.lock
generated
841
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,7 @@ If you use a plugin that _isn't_ Rojo for syncing code, open an issue and let me
|
|||||||
## Contributing
|
## Contributing
|
||||||
Pull requests are welcome!
|
Pull requests are welcome!
|
||||||
|
|
||||||
Rojo supports Rust 1.31.1 and newer. Any changes to the minimum required compiler version require a _minor_ version bump.
|
Rojo supports Rust 1.32 and newer. Any changes to the minimum required compiler version require a _minor_ version bump.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||||
@@ -25,7 +25,7 @@ If you have Rust installed, the easiest way to get Rojo is with Cargo!
|
|||||||
To install the latest 0.5.0 alpha, use:
|
To install the latest 0.5.0 alpha, use:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install rojo --version 0.5.0-alpha.3
|
cargo install rojo --version 0.5.0-alpha.5
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installing the Plugin
|
## Installing the Plugin
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ stds.roblox = {
|
|||||||
|
|
||||||
-- Types
|
-- Types
|
||||||
"Vector2", "Vector3",
|
"Vector2", "Vector3",
|
||||||
|
"Vector2int16", "Vector3int16",
|
||||||
"Color3",
|
"Color3",
|
||||||
"UDim", "UDim2",
|
"UDim", "UDim2",
|
||||||
"Rect",
|
"Rect",
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
local sheetAsset = "rbxassetid://2738712459"
|
|
||||||
|
|
||||||
local Assets = {
|
local Assets = {
|
||||||
Sprites = {
|
Sprites = {
|
||||||
WhiteCross = {
|
WhiteCross = {
|
||||||
asset = sheetAsset,
|
asset = "rbxassetid://2738712459",
|
||||||
offset = Vector2.new(190, 318),
|
offset = Vector2.new(190, 318),
|
||||||
size = Vector2.new(18, 18),
|
size = Vector2.new(18, 18),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ local Config = require(Plugin.Config)
|
|||||||
local Version = require(Plugin.Version)
|
local Version = require(Plugin.Version)
|
||||||
local Logging = require(Plugin.Logging)
|
local Logging = require(Plugin.Logging)
|
||||||
local DevSettings = require(Plugin.DevSettings)
|
local DevSettings = require(Plugin.DevSettings)
|
||||||
|
local preloadAssets = require(Plugin.preloadAssets)
|
||||||
|
|
||||||
local ConnectPanel = require(Plugin.Components.ConnectPanel)
|
local ConnectPanel = require(Plugin.Components.ConnectPanel)
|
||||||
local ConnectionActivePanel = require(Plugin.Components.ConnectionActivePanel)
|
local ConnectionActivePanel = require(Plugin.Components.ConnectionActivePanel)
|
||||||
@@ -177,6 +178,8 @@ function App:didMount()
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
preloadAssets()
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:didUpdate()
|
function App:didUpdate()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
return {
|
return {
|
||||||
codename = "Epiphany",
|
codename = "Epiphany",
|
||||||
version = {0, 5, 0, "-alpha.4"},
|
version = {0, 5, 0, "-alpha.5"},
|
||||||
expectedServerVersionString = "0.5.0 or newer",
|
expectedServerVersionString = "0.5.0 or newer",
|
||||||
protocolVersion = 2,
|
protocolVersion = 2,
|
||||||
defaultHost = "localhost",
|
defaultHost = "localhost",
|
||||||
|
|||||||
71
plugin/src/InstanceMap.lua
Normal file
71
plugin/src/InstanceMap.lua
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
local Logging = require(script.Parent.Logging)
|
||||||
|
|
||||||
|
--[[
|
||||||
|
A bidirectional map between instance IDs and Roblox instances. It lets us
|
||||||
|
keep track of every instance we know about.
|
||||||
|
|
||||||
|
TODO: Track ancestry to catch when stuff moves?
|
||||||
|
]]
|
||||||
|
local InstanceMap = {}
|
||||||
|
InstanceMap.__index = InstanceMap
|
||||||
|
|
||||||
|
function InstanceMap.new()
|
||||||
|
local self = {
|
||||||
|
fromIds = {},
|
||||||
|
fromInstances = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
return setmetatable(self, InstanceMap)
|
||||||
|
end
|
||||||
|
|
||||||
|
function InstanceMap:insert(id, instance)
|
||||||
|
self.fromIds[id] = instance
|
||||||
|
self.fromInstances[instance] = id
|
||||||
|
end
|
||||||
|
|
||||||
|
function InstanceMap:removeId(id)
|
||||||
|
local instance = self.fromIds[id]
|
||||||
|
|
||||||
|
if instance ~= nil then
|
||||||
|
self.fromIds[id] = nil
|
||||||
|
self.fromInstances[instance] = nil
|
||||||
|
else
|
||||||
|
Logging.warn("Attempted to remove nonexistant ID %s", tostring(id))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function InstanceMap:removeInstance(instance)
|
||||||
|
local id = self.fromInstances[instance]
|
||||||
|
|
||||||
|
if id ~= nil then
|
||||||
|
self.fromInstances[instance] = nil
|
||||||
|
self.fromIds[id] = nil
|
||||||
|
else
|
||||||
|
Logging.warn("Attempted to remove nonexistant instance %s", tostring(instance))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function InstanceMap:destroyId(id)
|
||||||
|
local instance = self.fromIds[id]
|
||||||
|
self:removeId(id)
|
||||||
|
|
||||||
|
if instance ~= nil then
|
||||||
|
local descendantsToDestroy = {}
|
||||||
|
|
||||||
|
for otherInstance in pairs(self.fromInstances) do
|
||||||
|
if otherInstance:IsDescendantOf(instance) then
|
||||||
|
table.insert(descendantsToDestroy, otherInstance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, otherInstance in ipairs(descendantsToDestroy) do
|
||||||
|
self:removeInstance(otherInstance)
|
||||||
|
end
|
||||||
|
|
||||||
|
instance:Destroy()
|
||||||
|
else
|
||||||
|
Logging.warn("Attempted to destroy nonexistant ID %s", tostring(id))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return InstanceMap
|
||||||
@@ -1,108 +1,14 @@
|
|||||||
|
local InstanceMap = require(script.Parent.InstanceMap)
|
||||||
local Logging = require(script.Parent.Logging)
|
local Logging = require(script.Parent.Logging)
|
||||||
|
local setProperty = require(script.Parent.setProperty)
|
||||||
local function makeInstanceMap()
|
local rojoValueToRobloxValue = require(script.Parent.rojoValueToRobloxValue)
|
||||||
local self = {
|
|
||||||
fromIds = {},
|
|
||||||
fromInstances = {},
|
|
||||||
}
|
|
||||||
|
|
||||||
function self:insert(id, instance)
|
|
||||||
self.fromIds[id] = instance
|
|
||||||
self.fromInstances[instance] = id
|
|
||||||
end
|
|
||||||
|
|
||||||
function self:removeId(id)
|
|
||||||
local instance = self.fromIds[id]
|
|
||||||
|
|
||||||
if instance ~= nil then
|
|
||||||
self.fromIds[id] = nil
|
|
||||||
self.fromInstances[instance] = nil
|
|
||||||
else
|
|
||||||
Logging.warn("Attempted to remove nonexistant ID %s", tostring(id))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function self:removeInstance(instance)
|
|
||||||
local id = self.fromInstances[instance]
|
|
||||||
|
|
||||||
if id ~= nil then
|
|
||||||
self.fromInstances[instance] = nil
|
|
||||||
self.fromIds[id] = nil
|
|
||||||
else
|
|
||||||
Logging.warn("Attempted to remove nonexistant instance %s", tostring(instance))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function self:destroyId(id)
|
|
||||||
local instance = self.fromIds[id]
|
|
||||||
self:removeId(id)
|
|
||||||
|
|
||||||
if instance ~= nil then
|
|
||||||
local descendantsToDestroy = {}
|
|
||||||
|
|
||||||
for otherInstance in pairs(self.fromInstances) do
|
|
||||||
if otherInstance:IsDescendantOf(instance) then
|
|
||||||
table.insert(descendantsToDestroy, otherInstance)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, otherInstance in ipairs(descendantsToDestroy) do
|
|
||||||
self:removeInstance(otherInstance)
|
|
||||||
end
|
|
||||||
|
|
||||||
instance:Destroy()
|
|
||||||
else
|
|
||||||
Logging.warn("Attempted to destroy nonexistant ID %s", tostring(id))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
local function setProperty(instance, key, value)
|
|
||||||
-- The 'Contents' property of LocalizationTable isn't directly exposed, but
|
|
||||||
-- has corresponding (deprecated) getters and setters.
|
|
||||||
if key == "Contents" and instance.ClassName == "LocalizationTable" then
|
|
||||||
instance:SetContents(value)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- If we don't have permissions to access this value at all, we can skip it.
|
|
||||||
local readSuccess, existingValue = pcall(function()
|
|
||||||
return instance[key]
|
|
||||||
end)
|
|
||||||
|
|
||||||
if not readSuccess then
|
|
||||||
-- An error will be thrown if there was a permission issue or if the
|
|
||||||
-- property doesn't exist. In the latter case, we should tell the user
|
|
||||||
-- because it's probably their fault.
|
|
||||||
if existingValue:find("lacking permission") then
|
|
||||||
Logging.trace("Permission error reading property %s on class %s", tostring(key), instance.ClassName)
|
|
||||||
return
|
|
||||||
else
|
|
||||||
error(("Invalid property %s on class %s: %s"):format(tostring(key), instance.ClassName, existingValue), 2)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local writeSuccess, err = pcall(function()
|
|
||||||
if existingValue ~= value then
|
|
||||||
instance[key] = value
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
if not writeSuccess then
|
|
||||||
error(("Cannot set property %s on class %s: %s"):format(tostring(key), instance.ClassName, err), 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
local Reconciler = {}
|
local Reconciler = {}
|
||||||
Reconciler.__index = Reconciler
|
Reconciler.__index = Reconciler
|
||||||
|
|
||||||
function Reconciler.new()
|
function Reconciler.new()
|
||||||
local self = {
|
local self = {
|
||||||
instanceMap = makeInstanceMap(),
|
instanceMap = InstanceMap.new(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return setmetatable(self, Reconciler)
|
return setmetatable(self, Reconciler)
|
||||||
@@ -140,7 +46,7 @@ function Reconciler:reconcile(virtualInstancesById, id, instance)
|
|||||||
setProperty(instance, "Name", virtualInstance.Name)
|
setProperty(instance, "Name", virtualInstance.Name)
|
||||||
|
|
||||||
for key, value in pairs(virtualInstance.Properties) do
|
for key, value in pairs(virtualInstance.Properties) do
|
||||||
setProperty(instance, key, value.Value)
|
setProperty(instance, key, rojoValueToRobloxValue(value))
|
||||||
end
|
end
|
||||||
|
|
||||||
local existingChildren = instance:GetChildren()
|
local existingChildren = instance:GetChildren()
|
||||||
@@ -195,9 +101,6 @@ function Reconciler:reconcile(virtualInstancesById, id, instance)
|
|||||||
-- Some instances, like services, don't like having their Parent
|
-- Some instances, like services, don't like having their Parent
|
||||||
-- property poked, even if we're setting it to the same value.
|
-- property poked, even if we're setting it to the same value.
|
||||||
setProperty(instance, "Parent", parent)
|
setProperty(instance, "Parent", parent)
|
||||||
if instance.Parent ~= parent then
|
|
||||||
instance.Parent = parent
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
@@ -217,8 +120,7 @@ function Reconciler:__reify(virtualInstancesById, id, parent)
|
|||||||
local instance = Instance.new(virtualInstance.ClassName)
|
local instance = Instance.new(virtualInstance.ClassName)
|
||||||
|
|
||||||
for key, value in pairs(virtualInstance.Properties) do
|
for key, value in pairs(virtualInstance.Properties) do
|
||||||
-- TODO: Branch on value.Type
|
setProperty(instance, key, rojoValueToRobloxValue(value))
|
||||||
setProperty(instance, key, value.Value)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
instance.Name = virtualInstance.Name
|
instance.Name = virtualInstance.Name
|
||||||
|
|||||||
28
plugin/src/preloadAssets.lua
Normal file
28
plugin/src/preloadAssets.lua
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
local ContentProvider = game:GetService("ContentProvider")
|
||||||
|
|
||||||
|
local Logging = require(script.Parent.Logging)
|
||||||
|
local Assets = require(script.Parent.Assets)
|
||||||
|
|
||||||
|
local function preloadAssets()
|
||||||
|
local contentUrls = {}
|
||||||
|
|
||||||
|
for _, sprite in pairs(Assets.Sprites) do
|
||||||
|
table.insert(contentUrls, sprite.asset)
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, slice in pairs(Assets.Slices) do
|
||||||
|
table.insert(contentUrls, slice.asset)
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, url in pairs(Assets.Images) do
|
||||||
|
table.insert(contentUrls, url)
|
||||||
|
end
|
||||||
|
|
||||||
|
Logging.trace("Preloading assets: %s", table.concat(contentUrls, ", "))
|
||||||
|
|
||||||
|
coroutine.wrap(function()
|
||||||
|
ContentProvider:PreloadAsync(contentUrls)
|
||||||
|
end)()
|
||||||
|
end
|
||||||
|
|
||||||
|
return preloadAssets
|
||||||
32
plugin/src/rojoValueToRobloxValue.lua
Normal file
32
plugin/src/rojoValueToRobloxValue.lua
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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 function rojoValueToRobloxValue(value)
|
||||||
|
if primitiveTypes[value.Type] then
|
||||||
|
return value.Value
|
||||||
|
end
|
||||||
|
|
||||||
|
local constructor = directConstructors[value.Type]
|
||||||
|
if constructor ~= nil then
|
||||||
|
return constructor(unpack(value.Value))
|
||||||
|
end
|
||||||
|
|
||||||
|
local errorMessage = ("The Rojo plugin doesn't know how to handle values of type %q yet!"):format(tostring(value.Type))
|
||||||
|
error(errorMessage)
|
||||||
|
end
|
||||||
|
|
||||||
|
return rojoValueToRobloxValue
|
||||||
40
plugin/src/rojoValueToRobloxValue.spec.lua
Normal file
40
plugin/src/rojoValueToRobloxValue.spec.lua
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
local rojoValueToRobloxValue = require(script.Parent.rojoValueToRobloxValue)
|
||||||
|
|
||||||
|
return function()
|
||||||
|
it("should convert primitives", function()
|
||||||
|
local inputString = {
|
||||||
|
Type = "String",
|
||||||
|
Value = "Hello, world!",
|
||||||
|
}
|
||||||
|
|
||||||
|
local inputFloat32 = {
|
||||||
|
Type = "Float32",
|
||||||
|
Value = 12341.512,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(rojoValueToRobloxValue(inputString)).to.equal(inputString.Value)
|
||||||
|
expect(rojoValueToRobloxValue(inputFloat32)).to.equal(inputFloat32.Value)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should convert properties with direct constructors", function()
|
||||||
|
local inputColor3 = {
|
||||||
|
Type = "Color3",
|
||||||
|
Value = {0, 1, 0.5},
|
||||||
|
}
|
||||||
|
local outputColor3 = Color3.new(0, 1, 0.5)
|
||||||
|
|
||||||
|
local inputCFrame = {
|
||||||
|
Type = "CFrame",
|
||||||
|
Value = {
|
||||||
|
1, 2, 3,
|
||||||
|
4, 5, 6,
|
||||||
|
7, 8, 9,
|
||||||
|
10, 11, 12,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
local outputCFrame = CFrame.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
|
||||||
|
|
||||||
|
expect(rojoValueToRobloxValue(inputColor3)).to.equal(outputColor3)
|
||||||
|
expect(rojoValueToRobloxValue(inputCFrame)).to.equal(outputCFrame)
|
||||||
|
end)
|
||||||
|
end
|
||||||
45
plugin/src/setProperty.lua
Normal file
45
plugin/src/setProperty.lua
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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.
|
||||||
|
]]
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
|
||||||
|
if not readSuccess then
|
||||||
|
-- An error will be thrown if there was a permission issue or if the
|
||||||
|
-- property doesn't exist. In the latter case, we should tell the user
|
||||||
|
-- because it's probably their fault.
|
||||||
|
if existingValue:find("lacking permission") then
|
||||||
|
Logging.trace("Permission error reading property %s on class %s", tostring(key), instance.ClassName)
|
||||||
|
return
|
||||||
|
else
|
||||||
|
error(("Invalid property %s on class %s: %s"):format(tostring(key), instance.ClassName, existingValue), 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local writeSuccess, err = pcall(function()
|
||||||
|
if existingValue ~= value then
|
||||||
|
instance[key] = value
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
if not writeSuccess then
|
||||||
|
error(("Cannot set property %s on class %s: %s"):format(tostring(key), instance.ClassName, err), 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return setProperty
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
local TestEZ = require(game.ReplicatedStorage.TestEZ)
|
local TestEZ = require(game.ReplicatedStorage.TestEZ)
|
||||||
TestEZ.TestBootstrap:run(game.ReplicatedStorage.Rojo.plugin)
|
TestEZ.TestBootstrap:run({game.ReplicatedStorage.Rojo.Plugin})
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "0.5.0-alpha.4"
|
version = "0.5.0-alpha.5"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
description = "A tool to create robust Roblox projects"
|
description = "A tool to create robust Roblox projects"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/LPGhatguy/rojo"
|
repository = "https://github.com/LPGhatguy/rojo"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
server-plugins = []
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "librojo"
|
name = "librojo"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
@@ -15,25 +19,25 @@ path = "src/lib.rs"
|
|||||||
name = "rojo"
|
name = "rojo"
|
||||||
path = "src/bin.rs"
|
path = "src/bin.rs"
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
bundle-plugin = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.27"
|
clap = "2.27"
|
||||||
csv = "1.0"
|
csv = "1.0"
|
||||||
env_logger = "0.6"
|
env_logger = "0.6"
|
||||||
failure = "0.1.3"
|
failure = "0.1.3"
|
||||||
|
futures = "0.1"
|
||||||
|
hyper = "0.12"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
maplit = "1.0.1"
|
maplit = "1.0.1"
|
||||||
notify = "4.0"
|
notify = "4.0"
|
||||||
rand = "0.4"
|
rand = "0.4"
|
||||||
rbx_binary = "0.2.0"
|
rbx_binary = "0.4.0"
|
||||||
rbx_tree = "0.2.0"
|
rbx_dom_weak = "1.0.0"
|
||||||
rbx_xml = "0.2.0"
|
rbx_xml = "0.4.0"
|
||||||
|
rbx_reflection = "2.0.374"
|
||||||
regex = "1.0"
|
regex = "1.0"
|
||||||
reqwest = "0.9.5"
|
reqwest = "0.9.5"
|
||||||
rouille = "2.1"
|
rlua = "0.16"
|
||||||
|
ritz = "0.1.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|||||||
43
server/assets/index.css
Normal file
43
server/assets/index.css
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: sans-serif;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 60rem;
|
||||||
|
background-color: #efefef;
|
||||||
|
border: 1px solid #666;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Rojo</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-family: sans-serif;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 60rem;
|
|
||||||
background-color: #efefef;
|
|
||||||
border: 1px solid #666;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.docs {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="main">
|
|
||||||
<h1 class="title">Rojo Live Sync is up and running!</h1>
|
|
||||||
<a class="docs" href="https://lpghatguy.github.io/rojo">Rojo Documentation</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -8,9 +8,10 @@ use log::info;
|
|||||||
use failure::Fail;
|
use failure::Fail;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
rbx_session::construct_oneoff_tree,
|
|
||||||
project::{Project, ProjectLoadFuzzyError},
|
|
||||||
imfs::{Imfs, FsError},
|
imfs::{Imfs, FsError},
|
||||||
|
project::{Project, ProjectLoadFuzzyError},
|
||||||
|
rbx_session::construct_oneoff_tree,
|
||||||
|
rbx_snapshot::SnapshotError,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -59,6 +60,9 @@ pub enum BuildError {
|
|||||||
|
|
||||||
#[fail(display = "{}", _0)]
|
#[fail(display = "{}", _0)]
|
||||||
FsError(#[fail(cause)] FsError),
|
FsError(#[fail(cause)] FsError),
|
||||||
|
|
||||||
|
#[fail(display = "{}", _0)]
|
||||||
|
SnapshotError(#[fail(cause)] SnapshotError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_from!(BuildError {
|
impl_from!(BuildError {
|
||||||
@@ -67,6 +71,7 @@ impl_from!(BuildError {
|
|||||||
rbx_xml::EncodeError => XmlModelEncodeError,
|
rbx_xml::EncodeError => XmlModelEncodeError,
|
||||||
rbx_binary::EncodeError => BinaryModelEncodeError,
|
rbx_binary::EncodeError => BinaryModelEncodeError,
|
||||||
FsError => FsError,
|
FsError => FsError,
|
||||||
|
SnapshotError => SnapshotError,
|
||||||
});
|
});
|
||||||
|
|
||||||
pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
|
pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
|
||||||
@@ -86,7 +91,7 @@ pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
|
|||||||
|
|
||||||
let mut imfs = Imfs::new();
|
let mut imfs = Imfs::new();
|
||||||
imfs.add_roots_from_project(&project)?;
|
imfs.add_roots_from_project(&project)?;
|
||||||
let tree = construct_oneoff_tree(&project, &imfs);
|
let tree = construct_oneoff_tree(&project, &imfs)?;
|
||||||
let mut file = File::create(&options.output_file)?;
|
let mut file = File::create(&options.output_file)?;
|
||||||
|
|
||||||
match output_kind {
|
match output_kind {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ use failure::Fail;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
project::{Project, ProjectLoadFuzzyError},
|
project::{Project, ProjectLoadFuzzyError},
|
||||||
web::Server,
|
web::LiveServer,
|
||||||
imfs::FsError,
|
imfs::FsError,
|
||||||
live_session::LiveSession,
|
live_session::{LiveSession, LiveSessionError},
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PORT: u16 = 34872;
|
const DEFAULT_PORT: u16 = 34872;
|
||||||
@@ -28,11 +28,15 @@ pub enum ServeError {
|
|||||||
|
|
||||||
#[fail(display = "{}", _0)]
|
#[fail(display = "{}", _0)]
|
||||||
FsError(#[fail(cause)] FsError),
|
FsError(#[fail(cause)] FsError),
|
||||||
|
|
||||||
|
#[fail(display = "{}", _0)]
|
||||||
|
LiveSessionError(#[fail(cause)] LiveSessionError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_from!(ServeError {
|
impl_from!(ServeError {
|
||||||
ProjectLoadFuzzyError => ProjectLoadError,
|
ProjectLoadFuzzyError => ProjectLoadError,
|
||||||
FsError => FsError,
|
FsError => FsError,
|
||||||
|
LiveSessionError => LiveSessionError,
|
||||||
});
|
});
|
||||||
|
|
||||||
pub fn serve(options: &ServeOptions) -> Result<(), ServeError> {
|
pub fn serve(options: &ServeOptions) -> Result<(), ServeError> {
|
||||||
@@ -45,7 +49,7 @@ pub fn serve(options: &ServeOptions) -> Result<(), ServeError> {
|
|||||||
info!("Using project {:#?}", project);
|
info!("Using project {:#?}", project);
|
||||||
|
|
||||||
let live_session = Arc::new(LiveSession::new(Arc::clone(&project))?);
|
let live_session = Arc::new(LiveSession::new(Arc::clone(&project))?);
|
||||||
let server = Server::new(Arc::clone(&live_session));
|
let server = LiveServer::new(live_session);
|
||||||
|
|
||||||
let port = options.port
|
let port = options.port
|
||||||
.or(project.serve_port)
|
.or(project.serve_port)
|
||||||
@@ -53,7 +57,7 @@ pub fn serve(options: &ServeOptions) -> Result<(), ServeError> {
|
|||||||
|
|
||||||
println!("Rojo server listening on port {}", port);
|
println!("Rojo server listening on port {}", port);
|
||||||
|
|
||||||
server.listen(port);
|
server.start(port);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -9,9 +9,10 @@ use failure::Fail;
|
|||||||
use reqwest::header::{ACCEPT, USER_AGENT, CONTENT_TYPE, COOKIE};
|
use reqwest::header::{ACCEPT, USER_AGENT, CONTENT_TYPE, COOKIE};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
rbx_session::construct_oneoff_tree,
|
|
||||||
project::{Project, ProjectLoadFuzzyError},
|
|
||||||
imfs::{Imfs, FsError},
|
imfs::{Imfs, FsError},
|
||||||
|
project::{Project, ProjectLoadFuzzyError},
|
||||||
|
rbx_session::construct_oneoff_tree,
|
||||||
|
rbx_snapshot::SnapshotError,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, Fail)]
|
||||||
@@ -36,6 +37,9 @@ pub enum UploadError {
|
|||||||
|
|
||||||
#[fail(display = "{}", _0)]
|
#[fail(display = "{}", _0)]
|
||||||
FsError(#[fail(cause)] FsError),
|
FsError(#[fail(cause)] FsError),
|
||||||
|
|
||||||
|
#[fail(display = "{}", _0)]
|
||||||
|
SnapshotError(#[fail(cause)] SnapshotError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_from!(UploadError {
|
impl_from!(UploadError {
|
||||||
@@ -44,6 +48,7 @@ impl_from!(UploadError {
|
|||||||
reqwest::Error => HttpError,
|
reqwest::Error => HttpError,
|
||||||
rbx_xml::EncodeError => XmlModelEncodeError,
|
rbx_xml::EncodeError => XmlModelEncodeError,
|
||||||
FsError => FsError,
|
FsError => FsError,
|
||||||
|
SnapshotError => SnapshotError,
|
||||||
});
|
});
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -67,7 +72,7 @@ pub fn upload(options: &UploadOptions) -> Result<(), UploadError> {
|
|||||||
|
|
||||||
let mut imfs = Imfs::new();
|
let mut imfs = Imfs::new();
|
||||||
imfs.add_roots_from_project(&project)?;
|
imfs.add_roots_from_project(&project)?;
|
||||||
let tree = construct_oneoff_tree(&project, &imfs);
|
let tree = construct_oneoff_tree(&project, &imfs)?;
|
||||||
|
|
||||||
let root_id = tree.get_root_id();
|
let root_id = tree.get_root_id();
|
||||||
let mut contents = Vec::new();
|
let mut contents = Vec::new();
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![recursion_limit="128"]
|
||||||
|
|
||||||
// Macros
|
// Macros
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod impl_from;
|
pub mod impl_from;
|
||||||
@@ -17,4 +19,3 @@ pub mod session_id;
|
|||||||
pub mod snapshot_reconciler;
|
pub mod snapshot_reconciler;
|
||||||
pub mod visualize;
|
pub mod visualize;
|
||||||
pub mod web;
|
pub mod web;
|
||||||
pub mod web_util;
|
|
||||||
@@ -1,21 +1,40 @@
|
|||||||
use std::{
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
mem,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use failure::Fail;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
fs_watcher::FsWatcher,
|
fs_watcher::FsWatcher,
|
||||||
imfs::{Imfs, FsError},
|
imfs::{Imfs, FsError},
|
||||||
message_queue::MessageQueue,
|
message_queue::MessageQueue,
|
||||||
project::Project,
|
project::Project,
|
||||||
rbx_session::RbxSession,
|
rbx_session::RbxSession,
|
||||||
|
rbx_snapshot::SnapshotError,
|
||||||
session_id::SessionId,
|
session_id::SessionId,
|
||||||
snapshot_reconciler::InstanceChanges,
|
snapshot_reconciler::InstanceChanges,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Fail)]
|
||||||
|
pub enum LiveSessionError {
|
||||||
|
#[fail(display = "{}", _0)]
|
||||||
|
Fs(#[fail(cause)] FsError),
|
||||||
|
|
||||||
|
#[fail(display = "{}", _0)]
|
||||||
|
Snapshot(#[fail(cause)] SnapshotError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_from!(LiveSessionError {
|
||||||
|
FsError => Fs,
|
||||||
|
SnapshotError => Snapshot,
|
||||||
|
});
|
||||||
|
|
||||||
/// Contains all of the state for a Rojo live-sync session.
|
/// Contains all of the state for a Rojo live-sync session.
|
||||||
pub struct LiveSession {
|
pub struct LiveSession {
|
||||||
pub project: Arc<Project>,
|
project: Arc<Project>,
|
||||||
pub session_id: SessionId,
|
session_id: SessionId,
|
||||||
pub message_queue: Arc<MessageQueue<InstanceChanges>>,
|
pub message_queue: Arc<MessageQueue<InstanceChanges>>,
|
||||||
pub rbx_session: Arc<Mutex<RbxSession>>,
|
pub rbx_session: Arc<Mutex<RbxSession>>,
|
||||||
pub imfs: Arc<Mutex<Imfs>>,
|
pub imfs: Arc<Mutex<Imfs>>,
|
||||||
@@ -23,7 +42,7 @@ pub struct LiveSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LiveSession {
|
impl LiveSession {
|
||||||
pub fn new(project: Arc<Project>) -> Result<LiveSession, FsError> {
|
pub fn new(project: Arc<Project>) -> Result<LiveSession, LiveSessionError> {
|
||||||
let imfs = {
|
let imfs = {
|
||||||
let mut imfs = Imfs::new();
|
let mut imfs = Imfs::new();
|
||||||
imfs.add_roots_from_project(&project)?;
|
imfs.add_roots_from_project(&project)?;
|
||||||
@@ -36,7 +55,7 @@ impl LiveSession {
|
|||||||
Arc::clone(&project),
|
Arc::clone(&project),
|
||||||
Arc::clone(&imfs),
|
Arc::clone(&imfs),
|
||||||
Arc::clone(&message_queue),
|
Arc::clone(&message_queue),
|
||||||
)));
|
)?));
|
||||||
|
|
||||||
let fs_watcher = FsWatcher::start(
|
let fs_watcher = FsWatcher::start(
|
||||||
Arc::clone(&imfs),
|
Arc::clone(&imfs),
|
||||||
@@ -46,8 +65,8 @@ impl LiveSession {
|
|||||||
let session_id = SessionId::new();
|
let session_id = SessionId::new();
|
||||||
|
|
||||||
Ok(LiveSession {
|
Ok(LiveSession {
|
||||||
project,
|
|
||||||
session_id,
|
session_id,
|
||||||
|
project,
|
||||||
message_queue,
|
message_queue,
|
||||||
rbx_session,
|
rbx_session,
|
||||||
imfs,
|
imfs,
|
||||||
@@ -55,7 +74,22 @@ impl LiveSession {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_project(&self) -> &Project {
|
/// Restarts the live session using the given project while preserving the
|
||||||
&self.project
|
/// internal session ID.
|
||||||
|
pub fn restart_with_new_project(&mut self, project: Arc<Project>) -> Result<(), LiveSessionError> {
|
||||||
|
let mut new_session = LiveSession::new(project)?;
|
||||||
|
new_session.session_id = self.session_id;
|
||||||
|
|
||||||
|
mem::replace(self, new_session);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_id(&self) -> SessionId {
|
||||||
|
self.session_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serve_place_ids(&self) -> &Option<HashSet<u64>> {
|
||||||
|
&self.project.serve_place_ids
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ use std::{
|
|||||||
use log::warn;
|
use log::warn;
|
||||||
use failure::Fail;
|
use failure::Fail;
|
||||||
use maplit::hashmap;
|
use maplit::hashmap;
|
||||||
use rbx_tree::RbxValue;
|
use rbx_dom_weak::{UnresolvedRbxValue, RbxValue};
|
||||||
use serde_derive::{Serialize, Deserialize};
|
use serde_derive::{Serialize, Deserialize};
|
||||||
|
|
||||||
pub static PROJECT_FILENAME: &'static str = "default.project.json";
|
pub static PROJECT_FILENAME: &'static str = "default.project.json";
|
||||||
@@ -24,6 +24,10 @@ struct SourceProject {
|
|||||||
name: String,
|
name: String,
|
||||||
tree: SourceProjectNode,
|
tree: SourceProjectNode,
|
||||||
|
|
||||||
|
#[cfg_attr(not(feature = "plugins-enabled"), serde(skip_deserializing))]
|
||||||
|
#[serde(default = "Vec::new", skip_serializing_if = "Vec::is_empty")]
|
||||||
|
plugins: Vec<SourcePlugin>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
serve_port: Option<u16>,
|
serve_port: Option<u16>,
|
||||||
|
|
||||||
@@ -33,12 +37,17 @@ struct SourceProject {
|
|||||||
|
|
||||||
impl SourceProject {
|
impl SourceProject {
|
||||||
/// Consumes the SourceProject and yields a Project, ready for prime-time.
|
/// Consumes the SourceProject and yields a Project, ready for prime-time.
|
||||||
pub fn into_project(self, project_file_location: &Path) -> Project {
|
pub fn into_project(mut self, project_file_location: &Path) -> Project {
|
||||||
let tree = self.tree.into_project_node(project_file_location);
|
let tree = self.tree.into_project_node(project_file_location);
|
||||||
|
let plugins = self.plugins
|
||||||
|
.drain(..)
|
||||||
|
.map(|source_plugin| source_plugin.into_plugin(project_file_location))
|
||||||
|
.collect();
|
||||||
|
|
||||||
Project {
|
Project {
|
||||||
name: self.name,
|
name: self.name,
|
||||||
tree,
|
tree,
|
||||||
|
plugins,
|
||||||
serve_port: self.serve_port,
|
serve_port: self.serve_port,
|
||||||
serve_place_ids: self.serve_place_ids,
|
serve_place_ids: self.serve_place_ids,
|
||||||
file_location: PathBuf::from(project_file_location),
|
file_location: PathBuf::from(project_file_location),
|
||||||
@@ -55,7 +64,7 @@ struct SourceProjectNode {
|
|||||||
class_name: Option<String>,
|
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")]
|
||||||
properties: HashMap<String, RbxValue>,
|
properties: HashMap<String, UnresolvedRbxValue>,
|
||||||
|
|
||||||
#[serde(rename = "$ignoreUnknownInstances", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "$ignoreUnknownInstances", skip_serializing_if = "Option::is_none")]
|
||||||
ignore_unknown_instances: Option<bool>,
|
ignore_unknown_instances: Option<bool>,
|
||||||
@@ -95,6 +104,26 @@ impl SourceProjectNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct SourcePlugin {
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourcePlugin {
|
||||||
|
pub fn into_plugin(self, project_file_location: &Path) -> Plugin {
|
||||||
|
let path = if Path::new(&self.path).is_absolute() {
|
||||||
|
PathBuf::from(self.path)
|
||||||
|
} else {
|
||||||
|
let project_folder_location = project_file_location.parent().unwrap();
|
||||||
|
project_folder_location.join(self.path)
|
||||||
|
};
|
||||||
|
|
||||||
|
Plugin {
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Error returned by Project::load_exact
|
/// Error returned by Project::load_exact
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, Fail)]
|
||||||
pub enum ProjectLoadExactError {
|
pub enum ProjectLoadExactError {
|
||||||
@@ -159,7 +188,7 @@ pub enum ProjectSaveError {
|
|||||||
pub struct ProjectNode {
|
pub struct ProjectNode {
|
||||||
pub class_name: Option<String>,
|
pub class_name: Option<String>,
|
||||||
pub children: HashMap<String, ProjectNode>,
|
pub children: HashMap<String, ProjectNode>,
|
||||||
pub properties: HashMap<String, RbxValue>,
|
pub properties: HashMap<String, UnresolvedRbxValue>,
|
||||||
pub ignore_unknown_instances: Option<bool>,
|
pub ignore_unknown_instances: Option<bool>,
|
||||||
|
|
||||||
#[serde(serialize_with = "crate::path_serializer::serialize_option")]
|
#[serde(serialize_with = "crate::path_serializer::serialize_option")]
|
||||||
@@ -198,10 +227,30 @@ impl ProjectNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Plugin {
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin {
|
||||||
|
fn to_source_plugin(&self, project_file_location: &Path) -> SourcePlugin {
|
||||||
|
let project_folder_location = project_file_location.parent().unwrap();
|
||||||
|
let path = match self.path.strip_prefix(project_folder_location) {
|
||||||
|
Ok(stripped) => stripped.to_str().unwrap().replace("\\", "/"),
|
||||||
|
Err(_) => format!("{}", self.path.display()),
|
||||||
|
};
|
||||||
|
|
||||||
|
SourcePlugin {
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub tree: ProjectNode,
|
pub tree: ProjectNode,
|
||||||
|
pub plugins: Vec<Plugin>,
|
||||||
pub serve_port: Option<u16>,
|
pub serve_port: Option<u16>,
|
||||||
pub serve_place_ids: Option<HashSet<u64>>,
|
pub serve_place_ids: Option<HashSet<u64>>,
|
||||||
pub file_location: PathBuf,
|
pub file_location: PathBuf,
|
||||||
@@ -235,7 +284,7 @@ impl Project {
|
|||||||
properties: hashmap! {
|
properties: hashmap! {
|
||||||
String::from("HttpEnabled") => RbxValue::Bool {
|
String::from("HttpEnabled") => RbxValue::Bool {
|
||||||
value: true,
|
value: true,
|
||||||
},
|
}.into(),
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -246,6 +295,7 @@ impl Project {
|
|||||||
let project = Project {
|
let project = Project {
|
||||||
name: project_name.to_string(),
|
name: project_name.to_string(),
|
||||||
tree,
|
tree,
|
||||||
|
plugins: Vec::new(),
|
||||||
serve_port: None,
|
serve_port: None,
|
||||||
serve_place_ids: None,
|
serve_place_ids: None,
|
||||||
file_location: project_path.clone(),
|
file_location: project_path.clone(),
|
||||||
@@ -274,6 +324,7 @@ impl Project {
|
|||||||
let project = Project {
|
let project = Project {
|
||||||
name: project_name.to_string(),
|
name: project_name.to_string(),
|
||||||
tree,
|
tree,
|
||||||
|
plugins: Vec::new(),
|
||||||
serve_port: None,
|
serve_port: None,
|
||||||
serve_place_ids: None,
|
serve_place_ids: None,
|
||||||
file_location: project_path.clone(),
|
file_location: project_path.clone(),
|
||||||
@@ -384,9 +435,15 @@ impl Project {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn to_source_project(&self) -> SourceProject {
|
fn to_source_project(&self) -> SourceProject {
|
||||||
|
let plugins = self.plugins
|
||||||
|
.iter()
|
||||||
|
.map(|plugin| plugin.to_source_plugin(&self.file_location))
|
||||||
|
.collect();
|
||||||
|
|
||||||
SourceProject {
|
SourceProject {
|
||||||
name: self.name.clone(),
|
name: self.name.clone(),
|
||||||
tree: self.tree.to_source_node(&self.file_location),
|
tree: self.tree.to_source_node(&self.file_location),
|
||||||
|
plugins,
|
||||||
serve_port: self.serve_port,
|
serve_port: self.serve_port,
|
||||||
serve_place_ids: self.serve_place_ids.clone(),
|
serve_place_ids: self.serve_place_ids.clone(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,25 @@ use std::{
|
|||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use rlua::Lua;
|
||||||
use serde_derive::{Serialize, Deserialize};
|
use serde_derive::{Serialize, Deserialize};
|
||||||
use log::{info, trace};
|
use log::{info, trace, error};
|
||||||
use rbx_tree::{RbxTree, RbxId};
|
use rbx_dom_weak::{RbxTree, RbxId};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
project::{Project, ProjectNode},
|
project::{Project, ProjectNode},
|
||||||
message_queue::MessageQueue,
|
message_queue::MessageQueue,
|
||||||
imfs::{Imfs, ImfsItem},
|
imfs::{Imfs, ImfsItem},
|
||||||
path_map::PathMap,
|
path_map::PathMap,
|
||||||
rbx_snapshot::{snapshot_project_tree, snapshot_project_node, snapshot_imfs_path},
|
rbx_snapshot::{
|
||||||
|
SnapshotError,
|
||||||
|
SnapshotContext,
|
||||||
|
SnapshotPluginContext,
|
||||||
|
SnapshotPluginEntry,
|
||||||
|
snapshot_project_tree,
|
||||||
|
snapshot_project_node,
|
||||||
|
snapshot_imfs_path,
|
||||||
|
},
|
||||||
snapshot_reconciler::{InstanceChanges, reify_root, reconcile_subtree},
|
snapshot_reconciler::{InstanceChanges, reify_root, reconcile_subtree},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,22 +67,60 @@ impl RbxSession {
|
|||||||
project: Arc<Project>,
|
project: Arc<Project>,
|
||||||
imfs: Arc<Mutex<Imfs>>,
|
imfs: Arc<Mutex<Imfs>>,
|
||||||
message_queue: Arc<MessageQueue<InstanceChanges>>,
|
message_queue: Arc<MessageQueue<InstanceChanges>>,
|
||||||
) -> RbxSession {
|
) -> Result<RbxSession, SnapshotError> {
|
||||||
let mut instances_per_path = PathMap::new();
|
let mut instances_per_path = PathMap::new();
|
||||||
let mut metadata_per_instance = HashMap::new();
|
let mut metadata_per_instance = HashMap::new();
|
||||||
|
|
||||||
let tree = {
|
let plugin_context = if cfg!(feature = "server-plugins") {
|
||||||
let temp_imfs = imfs.lock().unwrap();
|
let lua = Lua::new();
|
||||||
reify_initial_tree(&project, &temp_imfs, &mut instances_per_path, &mut metadata_per_instance)
|
let mut callback_key = None;
|
||||||
|
|
||||||
|
lua.context(|context| {
|
||||||
|
let callback = context.load(r#"
|
||||||
|
return function(snapshot)
|
||||||
|
print("got my snapshot:", snapshot)
|
||||||
|
print("name:", snapshot.name, "class name:", snapshot.className)
|
||||||
|
end"#)
|
||||||
|
.set_name("a cool plugin").unwrap()
|
||||||
|
.call::<(), rlua::Function>(()).unwrap();
|
||||||
|
|
||||||
|
callback_key = Some(context.create_registry_value(callback).unwrap());
|
||||||
|
});
|
||||||
|
|
||||||
|
let plugins = vec![
|
||||||
|
SnapshotPluginEntry {
|
||||||
|
file_name_filter: String::new(),
|
||||||
|
callback: callback_key.unwrap(),
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
Some(SnapshotPluginContext { lua, plugins })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
RbxSession {
|
let context = SnapshotContext {
|
||||||
|
plugin_context,
|
||||||
|
};
|
||||||
|
|
||||||
|
let tree = {
|
||||||
|
let temp_imfs = imfs.lock().unwrap();
|
||||||
|
reify_initial_tree(
|
||||||
|
&project,
|
||||||
|
&context,
|
||||||
|
&temp_imfs,
|
||||||
|
&mut instances_per_path,
|
||||||
|
&mut metadata_per_instance,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(RbxSession {
|
||||||
tree,
|
tree,
|
||||||
instances_per_path,
|
instances_per_path,
|
||||||
metadata_per_instance,
|
metadata_per_instance,
|
||||||
message_queue,
|
message_queue,
|
||||||
imfs,
|
imfs,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path_created_or_updated(&mut self, path: &Path) {
|
fn path_created_or_updated(&mut self, path: &Path) {
|
||||||
@@ -104,27 +151,37 @@ impl RbxSession {
|
|||||||
.expect("Metadata did not exist for path")
|
.expect("Metadata did not exist for path")
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
|
let context = SnapshotContext {
|
||||||
|
plugin_context: None,
|
||||||
|
};
|
||||||
|
|
||||||
for instance_id in &instances_at_path {
|
for instance_id in &instances_at_path {
|
||||||
let instance_metadata = self.metadata_per_instance.get(&instance_id)
|
let instance_metadata = self.metadata_per_instance.get(&instance_id)
|
||||||
.expect("Metadata for instance ID did not exist");
|
.expect("Metadata for instance ID did not exist");
|
||||||
|
|
||||||
let maybe_snapshot = match &instance_metadata.project_definition {
|
let maybe_snapshot = match &instance_metadata.project_definition {
|
||||||
Some((instance_name, project_node)) => {
|
Some((instance_name, project_node)) => {
|
||||||
snapshot_project_node(&imfs, &project_node, Cow::Owned(instance_name.clone()))
|
snapshot_project_node(&context, &imfs, &project_node, Cow::Owned(instance_name.clone()))
|
||||||
.unwrap_or_else(|_| panic!("Could not generate instance snapshot for path {}", path_to_snapshot.display()))
|
// .unwrap_or_else(|_| panic!("Could not generate instance snapshot for path {}", path_to_snapshot.display()))
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
snapshot_imfs_path(&imfs, &path_to_snapshot, None)
|
snapshot_imfs_path(&context, &imfs, &path_to_snapshot, None)
|
||||||
.unwrap_or_else(|_| panic!("Could not generate instance snapshot for path {}", path_to_snapshot.display()))
|
// .unwrap_or_else(|_| panic!("Could not generate instance snapshot for path {}", path_to_snapshot.display()))
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let snapshot = match maybe_snapshot {
|
let snapshot = match maybe_snapshot {
|
||||||
Some(snapshot) => snapshot,
|
Ok(Some(snapshot)) => snapshot,
|
||||||
None => {
|
Ok(None) => {
|
||||||
trace!("Path resulted in no snapshot being generated.");
|
trace!("Path resulted in no snapshot being generated.");
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("Rojo couldn't turn one of the project's files into Roblox instances.");
|
||||||
|
error!("Any changes to the file have been ignored.");
|
||||||
|
error!("{}", err);
|
||||||
|
return;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!("Snapshot: {:#?}", snapshot);
|
trace!("Snapshot: {:#?}", snapshot);
|
||||||
@@ -199,24 +256,30 @@ impl RbxSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn construct_oneoff_tree(project: &Project, imfs: &Imfs) -> RbxTree {
|
pub fn construct_oneoff_tree(project: &Project, imfs: &Imfs) -> Result<RbxTree, SnapshotError> {
|
||||||
let mut instances_per_path = PathMap::new();
|
let mut instances_per_path = PathMap::new();
|
||||||
let mut metadata_per_instance = HashMap::new();
|
let mut metadata_per_instance = HashMap::new();
|
||||||
reify_initial_tree(project, imfs, &mut instances_per_path, &mut metadata_per_instance)
|
let context = SnapshotContext {
|
||||||
|
plugin_context: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
reify_initial_tree(project, &context, imfs, &mut instances_per_path, &mut metadata_per_instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reify_initial_tree(
|
fn reify_initial_tree(
|
||||||
project: &Project,
|
project: &Project,
|
||||||
|
context: &SnapshotContext,
|
||||||
imfs: &Imfs,
|
imfs: &Imfs,
|
||||||
instances_per_path: &mut PathMap<HashSet<RbxId>>,
|
instances_per_path: &mut PathMap<HashSet<RbxId>>,
|
||||||
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
|
metadata_per_instance: &mut HashMap<RbxId, MetadataPerInstance>,
|
||||||
) -> RbxTree {
|
) -> Result<RbxTree, SnapshotError> {
|
||||||
let snapshot = snapshot_project_tree(imfs, project)
|
let snapshot = match snapshot_project_tree(&context, imfs, project)? {
|
||||||
.expect("Could not snapshot project tree")
|
Some(snapshot) => snapshot,
|
||||||
.expect("Project did not produce any instances");
|
None => panic!("Project did not produce any instances"),
|
||||||
|
};
|
||||||
|
|
||||||
let mut changes = InstanceChanges::default();
|
let mut changes = InstanceChanges::default();
|
||||||
let tree = reify_root(&snapshot, instances_per_path, metadata_per_instance, &mut changes);
|
let tree = reify_root(&snapshot, instances_per_path, metadata_per_instance, &mut changes);
|
||||||
|
|
||||||
tree
|
Ok(tree)
|
||||||
}
|
}
|
||||||
@@ -9,11 +9,13 @@ use std::{
|
|||||||
str,
|
str,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use rlua::Lua;
|
||||||
use failure::Fail;
|
use failure::Fail;
|
||||||
use log::info;
|
use log::info;
|
||||||
use maplit::hashmap;
|
use maplit::hashmap;
|
||||||
use rbx_tree::{RbxTree, RbxValue, RbxInstanceProperties};
|
use rbx_dom_weak::{RbxTree, RbxValue, RbxInstanceProperties};
|
||||||
use serde_derive::{Serialize, Deserialize};
|
use serde_derive::{Serialize, Deserialize};
|
||||||
|
use rbx_reflection::{try_resolve_value, ValueResolveError};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
imfs::{
|
imfs::{
|
||||||
@@ -38,6 +40,53 @@ const INIT_MODULE_NAME: &str = "init.lua";
|
|||||||
const INIT_SERVER_NAME: &str = "init.server.lua";
|
const INIT_SERVER_NAME: &str = "init.server.lua";
|
||||||
const INIT_CLIENT_NAME: &str = "init.client.lua";
|
const INIT_CLIENT_NAME: &str = "init.client.lua";
|
||||||
|
|
||||||
|
pub struct SnapshotContext {
|
||||||
|
pub plugin_context: Option<SnapshotPluginContext>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Context that's only relevant to generating snapshots if there are plugins
|
||||||
|
/// associated with the project.
|
||||||
|
///
|
||||||
|
/// It's possible that this needs some sort of extra nesting/filtering to
|
||||||
|
/// support nested projects, since their plugins should only apply to
|
||||||
|
/// themselves.
|
||||||
|
pub struct SnapshotPluginContext {
|
||||||
|
pub lua: Lua,
|
||||||
|
pub plugins: Vec<SnapshotPluginEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SnapshotPluginEntry {
|
||||||
|
/// Simple file name suffix filter to avoid running plugins on every file
|
||||||
|
/// change.
|
||||||
|
pub file_name_filter: String,
|
||||||
|
|
||||||
|
/// A key into the Lua registry created by [`create_registry_value`] that
|
||||||
|
/// refers to a function that can be called to transform a file/instance
|
||||||
|
/// pair according to how the plugin needs to operate.
|
||||||
|
///
|
||||||
|
/// [`create_registry_value`]: https://docs.rs/rlua/0.16.2/rlua/struct.Context.html#method.create_registry_value
|
||||||
|
pub callback: rlua::RegistryKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct LuaRbxSnapshot(RbxSnapshotInstance<'static>);
|
||||||
|
|
||||||
|
impl rlua::UserData for LuaRbxSnapshot {
|
||||||
|
fn add_methods<'lua, M: rlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||||
|
methods.add_meta_method(rlua::MetaMethod::Index, |_context, this, key: String| {
|
||||||
|
match key.as_str() {
|
||||||
|
"name" => Ok(this.0.name.clone().into_owned()),
|
||||||
|
"className" => Ok(this.0.class_name.clone().into_owned()),
|
||||||
|
_ => Err(rlua::Error::RuntimeError(format!("{} is not a valid member of RbxSnapshotInstance", &key))),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
methods.add_meta_method(rlua::MetaMethod::ToString, |_context, _this, _args: ()| {
|
||||||
|
Ok("RbxSnapshotInstance")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type SnapshotResult<'a> = Result<Option<RbxSnapshotInstance<'a>>, SnapshotError>;
|
pub type SnapshotResult<'a> = Result<Option<RbxSnapshotInstance<'a>>, SnapshotError>;
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, Fail)]
|
||||||
@@ -71,6 +120,19 @@ pub enum SnapshotError {
|
|||||||
ProjectNodeInvalidTransmute {
|
ProjectNodeInvalidTransmute {
|
||||||
partition_path: PathBuf,
|
partition_path: PathBuf,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
PropertyResolveError {
|
||||||
|
#[fail(cause)]
|
||||||
|
inner: ValueResolveError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ValueResolveError> for SnapshotError {
|
||||||
|
fn from(inner: ValueResolveError) -> SnapshotError {
|
||||||
|
SnapshotError::PropertyResolveError {
|
||||||
|
inner,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for SnapshotError {
|
impl fmt::Display for SnapshotError {
|
||||||
@@ -99,24 +161,27 @@ impl fmt::Display for SnapshotError {
|
|||||||
writeln!(output, "")?;
|
writeln!(output, "")?;
|
||||||
writeln!(output, "Partition target ($path): {}", partition_path.display())
|
writeln!(output, "Partition target ($path): {}", partition_path.display())
|
||||||
},
|
},
|
||||||
|
SnapshotError::PropertyResolveError { inner } => write!(output, "{}", inner),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn snapshot_project_tree<'source>(
|
pub fn snapshot_project_tree<'source>(
|
||||||
|
context: &SnapshotContext,
|
||||||
imfs: &'source Imfs,
|
imfs: &'source Imfs,
|
||||||
project: &'source Project,
|
project: &'source Project,
|
||||||
) -> SnapshotResult<'source> {
|
) -> SnapshotResult<'source> {
|
||||||
snapshot_project_node(imfs, &project.tree, Cow::Borrowed(&project.name))
|
snapshot_project_node(context, imfs, &project.tree, Cow::Borrowed(&project.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn snapshot_project_node<'source>(
|
pub fn snapshot_project_node<'source>(
|
||||||
|
context: &SnapshotContext,
|
||||||
imfs: &'source Imfs,
|
imfs: &'source Imfs,
|
||||||
node: &ProjectNode,
|
node: &ProjectNode,
|
||||||
instance_name: Cow<'source, str>,
|
instance_name: Cow<'source, str>,
|
||||||
) -> SnapshotResult<'source> {
|
) -> SnapshotResult<'source> {
|
||||||
let maybe_snapshot = match &node.path {
|
let maybe_snapshot = match &node.path {
|
||||||
Some(path) => snapshot_imfs_path(imfs, &path, Some(instance_name.clone()))?,
|
Some(path) => snapshot_imfs_path(context, imfs, &path, Some(instance_name.clone()))?,
|
||||||
None => match &node.class_name {
|
None => match &node.class_name {
|
||||||
Some(_class_name) => Some(RbxSnapshotInstance {
|
Some(_class_name) => Some(RbxSnapshotInstance {
|
||||||
name: instance_name.clone(),
|
name: instance_name.clone(),
|
||||||
@@ -170,13 +235,14 @@ pub fn snapshot_project_node<'source>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (child_name, child_project_node) in &node.children {
|
for (child_name, child_project_node) in &node.children {
|
||||||
if let Some(child) = snapshot_project_node(imfs, child_project_node, Cow::Owned(child_name.clone()))? {
|
if let Some(child) = snapshot_project_node(context, imfs, child_project_node, Cow::Owned(child_name.clone()))? {
|
||||||
snapshot.children.push(child);
|
snapshot.children.push(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (key, value) in &node.properties {
|
for (key, value) in &node.properties {
|
||||||
snapshot.properties.insert(key.clone(), value.clone());
|
let resolved_value = try_resolve_value(&snapshot.class_name, key, value)?;
|
||||||
|
snapshot.properties.insert(key.clone(), resolved_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ignore_unknown_instances) = node.ignore_unknown_instances {
|
if let Some(ignore_unknown_instances) = node.ignore_unknown_instances {
|
||||||
@@ -189,6 +255,7 @@ pub fn snapshot_project_node<'source>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn snapshot_imfs_path<'source>(
|
pub fn snapshot_imfs_path<'source>(
|
||||||
|
context: &SnapshotContext,
|
||||||
imfs: &'source Imfs,
|
imfs: &'source Imfs,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
instance_name: Option<Cow<'source, str>>,
|
instance_name: Option<Cow<'source, str>>,
|
||||||
@@ -196,23 +263,25 @@ pub fn snapshot_imfs_path<'source>(
|
|||||||
// If the given path doesn't exist in the in-memory filesystem, we consider
|
// If the given path doesn't exist in the in-memory filesystem, we consider
|
||||||
// that an error.
|
// that an error.
|
||||||
match imfs.get(path) {
|
match imfs.get(path) {
|
||||||
Some(imfs_item) => snapshot_imfs_item(imfs, imfs_item, instance_name),
|
Some(imfs_item) => snapshot_imfs_item(context, imfs, imfs_item, instance_name),
|
||||||
None => return Err(SnapshotError::DidNotExist(path.to_owned())),
|
None => return Err(SnapshotError::DidNotExist(path.to_owned())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot_imfs_item<'source>(
|
fn snapshot_imfs_item<'source>(
|
||||||
|
context: &SnapshotContext,
|
||||||
imfs: &'source Imfs,
|
imfs: &'source Imfs,
|
||||||
item: &'source ImfsItem,
|
item: &'source ImfsItem,
|
||||||
instance_name: Option<Cow<'source, str>>,
|
instance_name: Option<Cow<'source, str>>,
|
||||||
) -> SnapshotResult<'source> {
|
) -> SnapshotResult<'source> {
|
||||||
match item {
|
match item {
|
||||||
ImfsItem::File(file) => snapshot_imfs_file(file, instance_name),
|
ImfsItem::File(file) => snapshot_imfs_file(context, file, instance_name),
|
||||||
ImfsItem::Directory(directory) => snapshot_imfs_directory(imfs, directory, instance_name),
|
ImfsItem::Directory(directory) => snapshot_imfs_directory(context, imfs, directory, instance_name),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot_imfs_directory<'source>(
|
fn snapshot_imfs_directory<'source>(
|
||||||
|
context: &SnapshotContext,
|
||||||
imfs: &'source Imfs,
|
imfs: &'source Imfs,
|
||||||
directory: &'source ImfsDirectory,
|
directory: &'source ImfsDirectory,
|
||||||
instance_name: Option<Cow<'source, str>>,
|
instance_name: Option<Cow<'source, str>>,
|
||||||
@@ -229,11 +298,11 @@ fn snapshot_imfs_directory<'source>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mut snapshot = if directory.children.contains(&init_path) {
|
let mut snapshot = if directory.children.contains(&init_path) {
|
||||||
snapshot_imfs_path(imfs, &init_path, Some(snapshot_name))?.unwrap()
|
snapshot_imfs_path(context, imfs, &init_path, Some(snapshot_name))?.unwrap()
|
||||||
} else if directory.children.contains(&init_server_path) {
|
} else if directory.children.contains(&init_server_path) {
|
||||||
snapshot_imfs_path(imfs, &init_server_path, Some(snapshot_name))?.unwrap()
|
snapshot_imfs_path(context, imfs, &init_server_path, Some(snapshot_name))?.unwrap()
|
||||||
} else if directory.children.contains(&init_client_path) {
|
} else if directory.children.contains(&init_client_path) {
|
||||||
snapshot_imfs_path(imfs, &init_client_path, Some(snapshot_name))?.unwrap()
|
snapshot_imfs_path(context, imfs, &init_client_path, Some(snapshot_name))?.unwrap()
|
||||||
} else {
|
} else {
|
||||||
RbxSnapshotInstance {
|
RbxSnapshotInstance {
|
||||||
class_name: Cow::Borrowed("Folder"),
|
class_name: Cow::Borrowed("Folder"),
|
||||||
@@ -262,7 +331,7 @@ fn snapshot_imfs_directory<'source>(
|
|||||||
// them here.
|
// them here.
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(child) = snapshot_imfs_path(imfs, child_path, None)? {
|
if let Some(child) = snapshot_imfs_path(context, imfs, child_path, None)? {
|
||||||
snapshot.children.push(child);
|
snapshot.children.push(child);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -273,6 +342,7 @@ fn snapshot_imfs_directory<'source>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot_imfs_file<'source>(
|
fn snapshot_imfs_file<'source>(
|
||||||
|
context: &SnapshotContext,
|
||||||
file: &'source ImfsFile,
|
file: &'source ImfsFile,
|
||||||
instance_name: Option<Cow<'source, str>>,
|
instance_name: Option<Cow<'source, str>>,
|
||||||
) -> SnapshotResult<'source> {
|
) -> SnapshotResult<'source> {
|
||||||
@@ -308,6 +378,20 @@ fn snapshot_imfs_file<'source>(
|
|||||||
info!("File generated no snapshot: {}", file.path.display());
|
info!("File generated no snapshot: {}", file.path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(snapshot) = maybe_snapshot.as_ref() {
|
||||||
|
if let Some(plugin_context) = &context.plugin_context {
|
||||||
|
for plugin in &plugin_context.plugins {
|
||||||
|
let owned_snapshot = snapshot.get_owned();
|
||||||
|
let registry_key = &plugin.callback;
|
||||||
|
|
||||||
|
plugin_context.lua.context(move |context| {
|
||||||
|
let callback: rlua::Function = context.registry_value(registry_key).unwrap();
|
||||||
|
callback.call::<_, ()>(LuaRbxSnapshot(owned_snapshot)).unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(maybe_snapshot)
|
Ok(maybe_snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use std::{
|
|||||||
str,
|
str,
|
||||||
};
|
};
|
||||||
|
|
||||||
use rbx_tree::{RbxTree, RbxId, RbxInstanceProperties, RbxValue};
|
use rbx_dom_weak::{RbxTree, RbxId, RbxInstanceProperties, RbxValue};
|
||||||
use serde_derive::{Serialize, Deserialize};
|
use serde_derive::{Serialize, Deserialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -64,7 +64,7 @@ impl InstanceChanges {
|
|||||||
|
|
||||||
/// A lightweight, hierarchical representation of an instance that can be
|
/// A lightweight, hierarchical representation of an instance that can be
|
||||||
/// applied to the tree.
|
/// applied to the tree.
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct RbxSnapshotInstance<'a> {
|
pub struct RbxSnapshotInstance<'a> {
|
||||||
pub name: Cow<'a, str>,
|
pub name: Cow<'a, str>,
|
||||||
pub class_name: Cow<'a, str>,
|
pub class_name: Cow<'a, str>,
|
||||||
@@ -73,6 +73,22 @@ pub struct RbxSnapshotInstance<'a> {
|
|||||||
pub metadata: MetadataPerInstance,
|
pub metadata: MetadataPerInstance,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> RbxSnapshotInstance<'a> {
|
||||||
|
pub fn get_owned(&'a self) -> RbxSnapshotInstance<'static> {
|
||||||
|
let children: Vec<RbxSnapshotInstance<'static>> = self.children.iter()
|
||||||
|
.map(RbxSnapshotInstance::get_owned)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
RbxSnapshotInstance {
|
||||||
|
name: Cow::Owned(self.name.clone().into_owned()),
|
||||||
|
class_name: Cow::Owned(self.class_name.clone().into_owned()),
|
||||||
|
properties: self.properties.clone(),
|
||||||
|
children,
|
||||||
|
metadata: self.metadata.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> PartialOrd for RbxSnapshotInstance<'a> {
|
impl<'a> PartialOrd for RbxSnapshotInstance<'a> {
|
||||||
fn partial_cmp(&self, other: &RbxSnapshotInstance) -> Option<Ordering> {
|
fn partial_cmp(&self, other: &RbxSnapshotInstance) -> Option<Ordering> {
|
||||||
Some(self.name.cmp(&other.name)
|
Some(self.name.cmp(&other.name)
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use rbx_tree::RbxId;
|
use rbx_dom_weak::RbxId;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
imfs::{Imfs, ImfsItem},
|
imfs::{Imfs, ImfsItem},
|
||||||
rbx_session::RbxSession,
|
rbx_session::RbxSession,
|
||||||
web::PublicInstanceMetadata,
|
web::api::PublicInstanceMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
static GRAPHVIZ_HEADER: &str = r#"
|
static GRAPHVIZ_HEADER: &str = r#"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//! Defines Rojo's web interface that all clients use to communicate with a
|
//! Defines Rojo's HTTP API, all under /api. These endpoints generally return
|
||||||
//! running live-sync session.
|
//! JSON.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
@@ -7,26 +7,26 @@ use std::{
|
|||||||
sync::{mpsc, Arc},
|
sync::{mpsc, Arc},
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde_derive::{Serialize, Deserialize};
|
use futures::{future, Future};
|
||||||
use log::trace;
|
use hyper::{
|
||||||
use rouille::{
|
service::Service,
|
||||||
self,
|
header,
|
||||||
router,
|
StatusCode,
|
||||||
|
Method,
|
||||||
|
Body,
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
};
|
};
|
||||||
use rbx_tree::{RbxId, RbxInstance};
|
use serde_derive::{Serialize, Deserialize};
|
||||||
|
use rbx_dom_weak::{RbxId, RbxInstance};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
live_session::LiveSession,
|
live_session::LiveSession,
|
||||||
session_id::SessionId,
|
session_id::SessionId,
|
||||||
snapshot_reconciler::InstanceChanges,
|
snapshot_reconciler::InstanceChanges,
|
||||||
visualize::{VisualizeRbxSession, VisualizeImfs, graphviz_to_svg},
|
|
||||||
rbx_session::{MetadataPerInstance},
|
rbx_session::{MetadataPerInstance},
|
||||||
};
|
};
|
||||||
|
|
||||||
static HOME_CONTENT: &str = include_str!("../assets/index.html");
|
|
||||||
|
|
||||||
/// Contains the instance metadata relevant to Rojo clients.
|
/// Contains the instance metadata relevant to Rojo clients.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -43,7 +43,7 @@ impl PublicInstanceMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Used to attach metadata specific to Rojo to instances, which come from the
|
/// Used to attach metadata specific to Rojo to instances, which come from the
|
||||||
/// rbx_tree crate.
|
/// rbx_dom_weak crate.
|
||||||
///
|
///
|
||||||
/// Both fields are wrapped in Cow in order to make owned-vs-borrowed simpler
|
/// Both fields are wrapped in Cow in order to make owned-vs-borrowed simpler
|
||||||
/// for tests.
|
/// for tests.
|
||||||
@@ -82,78 +82,89 @@ pub struct SubscribeResponse<'a> {
|
|||||||
pub messages: Cow<'a, [InstanceChanges]>,
|
pub messages: Cow<'a, [InstanceChanges]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Server {
|
fn response_json<T: serde::Serialize>(value: T) -> Response<Body> {
|
||||||
|
let serialized = match serde_json::to_string(&value) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::BAD_REQUEST)
|
||||||
|
.header(header::CONTENT_TYPE, "text/plain")
|
||||||
|
.body(Body::from(err.to_string()))
|
||||||
|
.unwrap();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Response::builder()
|
||||||
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
|
.body(Body::from(serialized))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ApiService {
|
||||||
live_session: Arc<LiveSession>,
|
live_session: Arc<LiveSession>,
|
||||||
server_version: &'static str,
|
server_version: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Service for ApiService {
|
||||||
pub fn new(live_session: Arc<LiveSession>) -> Server {
|
type ReqBody = Body;
|
||||||
Server {
|
type ResBody = Body;
|
||||||
|
type Error = hyper::Error;
|
||||||
|
type Future = Box<dyn Future<Item = hyper::Response<Self::ReqBody>, Error = Self::Error> + Send>;
|
||||||
|
|
||||||
|
fn call(&mut self, request: hyper::Request<Self::ReqBody>) -> Self::Future {
|
||||||
|
let response = match (request.method(), request.uri().path()) {
|
||||||
|
(&Method::GET, "/api/rojo") => self.handle_api_rojo(),
|
||||||
|
(&Method::GET, path) if path.starts_with("/api/subscribe/") => self.handle_api_subscribe(request),
|
||||||
|
(&Method::GET, path) if path.starts_with("/api/read/") => self.handle_api_read(request),
|
||||||
|
_ => {
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Box::new(future::ok(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiService {
|
||||||
|
pub fn new(live_session: Arc<LiveSession>) -> ApiService {
|
||||||
|
ApiService {
|
||||||
live_session,
|
live_session,
|
||||||
server_version: env!("CARGO_PKG_VERSION"),
|
server_version: env!("CARGO_PKG_VERSION"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unreachable_code)]
|
|
||||||
pub fn handle_request(&self, request: &Request) -> Response {
|
|
||||||
trace!("Request {} {}", request.method(), request.url());
|
|
||||||
|
|
||||||
router!(request,
|
|
||||||
(GET) (/) => {
|
|
||||||
self.handle_home()
|
|
||||||
},
|
|
||||||
(GET) (/api/rojo) => {
|
|
||||||
self.handle_api_rojo()
|
|
||||||
},
|
|
||||||
(GET) (/api/subscribe/{ cursor: u32 }) => {
|
|
||||||
self.handle_api_subscribe(cursor)
|
|
||||||
},
|
|
||||||
(GET) (/api/read/{ id_list: String }) => {
|
|
||||||
let requested_ids: Option<Vec<RbxId>> = id_list
|
|
||||||
.split(',')
|
|
||||||
.map(RbxId::parse_str)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
self.handle_api_read(requested_ids)
|
|
||||||
},
|
|
||||||
(GET) (/visualize/rbx) => {
|
|
||||||
self.handle_visualize_rbx()
|
|
||||||
},
|
|
||||||
(GET) (/visualize/imfs) => {
|
|
||||||
self.handle_visualize_imfs()
|
|
||||||
},
|
|
||||||
_ => Response::empty_404()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn listen(self, port: u16) {
|
|
||||||
let address = format!("0.0.0.0:{}", port);
|
|
||||||
|
|
||||||
rouille::start_server(address, move |request| self.handle_request(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_home(&self) -> Response {
|
|
||||||
Response::html(HOME_CONTENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a summary of information about the server
|
/// Get a summary of information about the server
|
||||||
fn handle_api_rojo(&self) -> Response {
|
fn handle_api_rojo(&self) -> Response<Body> {
|
||||||
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
||||||
let tree = rbx_session.get_tree();
|
let tree = rbx_session.get_tree();
|
||||||
|
|
||||||
Response::json(&ServerInfoResponse {
|
response_json(&ServerInfoResponse {
|
||||||
server_version: self.server_version,
|
server_version: self.server_version,
|
||||||
protocol_version: 2,
|
protocol_version: 2,
|
||||||
session_id: self.live_session.session_id,
|
session_id: self.live_session.session_id(),
|
||||||
expected_place_ids: self.live_session.project.serve_place_ids.clone(),
|
expected_place_ids: self.live_session.serve_place_ids().clone(),
|
||||||
root_instance_id: tree.get_root_id(),
|
root_instance_id: tree.get_root_id(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve any messages past the given cursor index, and if
|
/// Retrieve any messages past the given cursor index, and if
|
||||||
/// there weren't any, subscribe to receive any new messages.
|
/// there weren't any, subscribe to receive any new messages.
|
||||||
fn handle_api_subscribe(&self, cursor: u32) -> Response {
|
fn handle_api_subscribe(&self, request: Request<Body>) -> Response<Body> {
|
||||||
|
let argument = &request.uri().path()["/api/subscribe/".len()..];
|
||||||
|
let cursor: u32 = match argument.parse() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::BAD_REQUEST)
|
||||||
|
.header(header::CONTENT_TYPE, "text/plain")
|
||||||
|
.body(Body::from(err.to_string()))
|
||||||
|
.unwrap();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let message_queue = Arc::clone(&self.live_session.message_queue);
|
let message_queue = Arc::clone(&self.live_session.message_queue);
|
||||||
|
|
||||||
// Did the client miss any messages since the last subscribe?
|
// Did the client miss any messages since the last subscribe?
|
||||||
@@ -161,21 +172,24 @@ impl Server {
|
|||||||
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
|
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
|
||||||
|
|
||||||
if !new_messages.is_empty() {
|
if !new_messages.is_empty() {
|
||||||
return Response::json(&SubscribeResponse {
|
return response_json(&SubscribeResponse {
|
||||||
session_id: self.live_session.session_id,
|
session_id: self.live_session.session_id(),
|
||||||
messages: Cow::Borrowed(&new_messages),
|
messages: Cow::Borrowed(&new_messages),
|
||||||
message_cursor: new_cursor,
|
message_cursor: new_cursor,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TOOD: Switch to futures mpsc instead to not block this task
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
let sender_id = message_queue.subscribe(tx);
|
let sender_id = message_queue.subscribe(tx);
|
||||||
|
|
||||||
match rx.recv() {
|
match rx.recv() {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(_) => return Response::text("error!").with_status_code(500),
|
Err(_) => return Response::builder()
|
||||||
|
.status(500)
|
||||||
|
.body(Body::from("error!"))
|
||||||
|
.unwrap(),
|
||||||
}
|
}
|
||||||
|
|
||||||
message_queue.unsubscribe(sender_id);
|
message_queue.unsubscribe(sender_id);
|
||||||
@@ -183,20 +197,32 @@ impl Server {
|
|||||||
{
|
{
|
||||||
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
|
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
|
||||||
|
|
||||||
return Response::json(&SubscribeResponse {
|
return response_json(&SubscribeResponse {
|
||||||
session_id: self.live_session.session_id,
|
session_id: self.live_session.session_id(),
|
||||||
messages: Cow::Owned(new_messages),
|
messages: Cow::Owned(new_messages),
|
||||||
message_cursor: new_cursor,
|
message_cursor: new_cursor,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_api_read(&self, requested_ids: Option<Vec<RbxId>>) -> Response {
|
fn handle_api_read(&self, request: Request<Body>) -> Response<Body> {
|
||||||
|
let argument = &request.uri().path()["/api/read/".len()..];
|
||||||
|
let requested_ids: Option<Vec<RbxId>> = argument
|
||||||
|
.split(',')
|
||||||
|
.map(RbxId::parse_str)
|
||||||
|
.collect();
|
||||||
|
|
||||||
let message_queue = Arc::clone(&self.live_session.message_queue);
|
let message_queue = Arc::clone(&self.live_session.message_queue);
|
||||||
|
|
||||||
let requested_ids = match requested_ids {
|
let requested_ids = match requested_ids {
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => return rouille::Response::text("Malformed ID list").with_status_code(400),
|
None => {
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::BAD_REQUEST)
|
||||||
|
.header(header::CONTENT_TYPE, "text/plain")
|
||||||
|
.body(Body::from("Malformed ID list"))
|
||||||
|
.unwrap();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
||||||
@@ -228,30 +254,10 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Response::json(&ReadResponse {
|
response_json(&ReadResponse {
|
||||||
session_id: self.live_session.session_id,
|
session_id: self.live_session.session_id(),
|
||||||
message_cursor,
|
message_cursor,
|
||||||
instances,
|
instances,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_visualize_rbx(&self) -> Response {
|
|
||||||
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
|
||||||
let dot_source = format!("{}", VisualizeRbxSession(&rbx_session));
|
|
||||||
|
|
||||||
match graphviz_to_svg(&dot_source) {
|
|
||||||
Some(svg) => Response::svg(svg),
|
|
||||||
None => Response::text(dot_source),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_visualize_imfs(&self) -> Response {
|
|
||||||
let imfs = self.live_session.imfs.lock().unwrap();
|
|
||||||
let dot_source = format!("{}", VisualizeImfs(&imfs));
|
|
||||||
|
|
||||||
match graphviz_to_svg(&dot_source) {
|
|
||||||
Some(svg) => Response::svg(svg),
|
|
||||||
None => Response::text(dot_source),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
121
server/src/web/interface.rs
Normal file
121
server/src/web/interface.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
//! Defines the HTTP-based UI. These endpoints generally return HTML and SVG.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use futures::{future, Future};
|
||||||
|
use hyper::{
|
||||||
|
service::Service,
|
||||||
|
header,
|
||||||
|
Body,
|
||||||
|
Method,
|
||||||
|
StatusCode,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
};
|
||||||
|
use ritz::html;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
live_session::LiveSession,
|
||||||
|
visualize::{VisualizeRbxSession, VisualizeImfs, graphviz_to_svg},
|
||||||
|
};
|
||||||
|
|
||||||
|
static HOME_CSS: &str = include_str!("../../assets/index.css");
|
||||||
|
|
||||||
|
pub struct InterfaceService {
|
||||||
|
live_session: Arc<LiveSession>,
|
||||||
|
server_version: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Service for InterfaceService {
|
||||||
|
type ReqBody = Body;
|
||||||
|
type ResBody = Body;
|
||||||
|
type Error = hyper::Error;
|
||||||
|
type Future = Box<dyn Future<Item = Response<Self::ReqBody>, Error = Self::Error> + Send>;
|
||||||
|
|
||||||
|
fn call(&mut self, request: Request<Self::ReqBody>) -> Self::Future {
|
||||||
|
let response = match (request.method(), request.uri().path()) {
|
||||||
|
(&Method::GET, "/") => self.handle_home(),
|
||||||
|
(&Method::GET, "/visualize/rbx") => self.handle_visualize_rbx(),
|
||||||
|
(&Method::GET, "/visualize/imfs") => self.handle_visualize_imfs(),
|
||||||
|
_ => Response::builder()
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Box::new(future::ok(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InterfaceService {
|
||||||
|
pub fn new(live_session: Arc<LiveSession>) -> InterfaceService {
|
||||||
|
InterfaceService {
|
||||||
|
live_session,
|
||||||
|
server_version: env!("CARGO_PKG_VERSION"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_home(&self) -> Response<Body> {
|
||||||
|
let page = html! {
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>"Rojo"</title>
|
||||||
|
<style>
|
||||||
|
{ ritz::UnescapedText::new(HOME_CSS) }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="main">
|
||||||
|
<h1 class="title">
|
||||||
|
"Rojo Live Sync is up and running!"
|
||||||
|
</h1>
|
||||||
|
<h2 class="subtitle">
|
||||||
|
"Version " { self.server_version }
|
||||||
|
</h2>
|
||||||
|
<a class="docs" href="https://lpghatguy.github.io/rojo">
|
||||||
|
"Rojo Documentation"
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
};
|
||||||
|
|
||||||
|
Response::builder()
|
||||||
|
.header(header::CONTENT_TYPE, "text/html")
|
||||||
|
.body(Body::from(format!("<!DOCTYPE html>{}", page)))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_visualize_rbx(&self) -> Response<Body> {
|
||||||
|
let rbx_session = self.live_session.rbx_session.lock().unwrap();
|
||||||
|
let dot_source = format!("{}", VisualizeRbxSession(&rbx_session));
|
||||||
|
|
||||||
|
match graphviz_to_svg(&dot_source) {
|
||||||
|
Some(svg) => Response::builder()
|
||||||
|
.header(header::CONTENT_TYPE, "image/svg+xml")
|
||||||
|
.body(Body::from(svg))
|
||||||
|
.unwrap(),
|
||||||
|
None => Response::builder()
|
||||||
|
.header(header::CONTENT_TYPE, "text/plain")
|
||||||
|
.body(Body::from(dot_source))
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_visualize_imfs(&self) -> Response<Body> {
|
||||||
|
let imfs = self.live_session.imfs.lock().unwrap();
|
||||||
|
let dot_source = format!("{}", VisualizeImfs(&imfs));
|
||||||
|
|
||||||
|
match graphviz_to_svg(&dot_source) {
|
||||||
|
Some(svg) => Response::builder()
|
||||||
|
.header(header::CONTENT_TYPE, "image/svg+xml")
|
||||||
|
.body(Body::from(svg))
|
||||||
|
.unwrap(),
|
||||||
|
None => Response::builder()
|
||||||
|
.header(header::CONTENT_TYPE, "text/plain")
|
||||||
|
.body(Body::from(dot_source))
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
85
server/src/web/mod.rs
Normal file
85
server/src/web/mod.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// TODO: This module needs to be public for visualize, we should move
|
||||||
|
// PublicInstanceMetadata and switch this private!
|
||||||
|
pub mod api;
|
||||||
|
mod interface;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use log::trace;
|
||||||
|
use futures::{
|
||||||
|
future::{self, FutureResult},
|
||||||
|
Future,
|
||||||
|
};
|
||||||
|
use hyper::{
|
||||||
|
service::Service,
|
||||||
|
Body,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
Server,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
live_session::LiveSession,
|
||||||
|
};
|
||||||
|
|
||||||
|
use self::{
|
||||||
|
api::ApiService,
|
||||||
|
interface::InterfaceService,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct RootService {
|
||||||
|
api: api::ApiService,
|
||||||
|
interface: interface::InterfaceService,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Service for RootService {
|
||||||
|
type ReqBody = Body;
|
||||||
|
type ResBody = Body;
|
||||||
|
type Error = hyper::Error;
|
||||||
|
type Future = Box<dyn Future<Item = Response<Self::ReqBody>, Error = Self::Error> + Send>;
|
||||||
|
|
||||||
|
fn call(&mut self, request: Request<Self::ReqBody>) -> Self::Future {
|
||||||
|
trace!("{} {}", request.method(), request.uri().path());
|
||||||
|
|
||||||
|
if request.uri().path().starts_with("/api") {
|
||||||
|
self.api.call(request)
|
||||||
|
} else {
|
||||||
|
self.interface.call(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RootService {
|
||||||
|
pub fn new(live_session: Arc<LiveSession>) -> RootService {
|
||||||
|
RootService {
|
||||||
|
api: ApiService::new(Arc::clone(&live_session)),
|
||||||
|
interface: InterfaceService::new(Arc::clone(&live_session)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LiveServer {
|
||||||
|
live_session: Arc<LiveSession>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LiveServer {
|
||||||
|
pub fn new(live_session: Arc<LiveSession>) -> LiveServer {
|
||||||
|
LiveServer {
|
||||||
|
live_session,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(self, port: u16) {
|
||||||
|
let address = ([127, 0, 0, 1], port).into();
|
||||||
|
|
||||||
|
let server = Server::bind(&address)
|
||||||
|
.serve(move || {
|
||||||
|
let service: FutureResult<_, hyper::Error> =
|
||||||
|
future::ok(RootService::new(Arc::clone(&self.live_session)));
|
||||||
|
service
|
||||||
|
})
|
||||||
|
.map_err(|e| eprintln!("Server error: {}", e));
|
||||||
|
|
||||||
|
hyper::rt::run(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
use std::io::Read;
|
|
||||||
|
|
||||||
use rouille;
|
|
||||||
use serde;
|
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
static MAX_BODY_SIZE: usize = 100 * 1024 * 1024; // 100 MiB
|
|
||||||
|
|
||||||
/// Pulls text that may be JSON out of a Rouille Request object.
|
|
||||||
///
|
|
||||||
/// Doesn't do any actual parsing -- all this method does is verify the content
|
|
||||||
/// type of the request and read the request's body.
|
|
||||||
fn read_json_text(request: &rouille::Request) -> Option<String> {
|
|
||||||
// Bail out if the request body isn't marked as JSON
|
|
||||||
let content_type = request.header("Content-Type")?;
|
|
||||||
|
|
||||||
if !content_type.starts_with("application/json") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let body = request.data()?;
|
|
||||||
|
|
||||||
// Allocate a buffer and read up to MAX_BODY_SIZE+1 bytes into it.
|
|
||||||
let mut out = Vec::new();
|
|
||||||
body.take(MAX_BODY_SIZE.saturating_add(1) as u64).read_to_end(&mut out).ok()?;
|
|
||||||
|
|
||||||
// If the body was too big (MAX_BODY_SIZE+1), we abort instead of trying to
|
|
||||||
// process it.
|
|
||||||
if out.len() > MAX_BODY_SIZE {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
String::from_utf8(out).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads the body out of a Rouille Request and attempts to turn it into JSON.
|
|
||||||
pub fn read_json<T>(request: &rouille::Request) -> Option<T>
|
|
||||||
where
|
|
||||||
T: serde::de::DeserializeOwned,
|
|
||||||
{
|
|
||||||
let body = read_json_text(&request)?;
|
|
||||||
serde_json::from_str(&body).ok()?
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use rbx_tree::RbxValue;
|
use rbx_dom_weak::RbxValue;
|
||||||
|
|
||||||
use librojo::{
|
use librojo::{
|
||||||
project::{Project, ProjectNode},
|
project::{Project, ProjectNode},
|
||||||
@@ -65,7 +65,7 @@ fn single_partition_game() {
|
|||||||
let mut http_service_properties = HashMap::new();
|
let mut http_service_properties = HashMap::new();
|
||||||
http_service_properties.insert("HttpEnabled".to_string(), RbxValue::Bool {
|
http_service_properties.insert("HttpEnabled".to_string(), RbxValue::Bool {
|
||||||
value: true,
|
value: true,
|
||||||
});
|
}.into());
|
||||||
|
|
||||||
let http_service = ProjectNode {
|
let http_service = ProjectNode {
|
||||||
class_name: Some(String::from("HttpService")),
|
class_name: Some(String::from("HttpService")),
|
||||||
@@ -86,6 +86,7 @@ fn single_partition_game() {
|
|||||||
Project {
|
Project {
|
||||||
name: "single-sync-point".to_string(),
|
name: "single-sync-point".to_string(),
|
||||||
tree: root_node,
|
tree: root_node,
|
||||||
|
plugins: Vec::new(),
|
||||||
serve_port: None,
|
serve_port: None,
|
||||||
serve_place_ids: None,
|
serve_place_ids: None,
|
||||||
file_location: project_location.join("default.project.json"),
|
file_location: project_location.join("default.project.json"),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use pretty_assertions::assert_eq;
|
|||||||
use librojo::{
|
use librojo::{
|
||||||
imfs::Imfs,
|
imfs::Imfs,
|
||||||
project::{Project, ProjectNode},
|
project::{Project, ProjectNode},
|
||||||
rbx_snapshot::snapshot_project_tree,
|
rbx_snapshot::{SnapshotContext, snapshot_project_tree},
|
||||||
snapshot_reconciler::{RbxSnapshotInstance},
|
snapshot_reconciler::{RbxSnapshotInstance},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,7 +47,11 @@ fn run_snapshot_test(path: &Path) {
|
|||||||
imfs.add_roots_from_project(&project)
|
imfs.add_roots_from_project(&project)
|
||||||
.expect("Could not add IMFS roots to snapshot project");
|
.expect("Could not add IMFS roots to snapshot project");
|
||||||
|
|
||||||
let mut snapshot = snapshot_project_tree(&imfs, &project)
|
let context = SnapshotContext {
|
||||||
|
plugin_context: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut snapshot = snapshot_project_tree(&context, &imfs, &project)
|
||||||
.expect("Could not generate snapshot for snapshot test");
|
.expect("Could not generate snapshot for snapshot test");
|
||||||
|
|
||||||
if let Some(snapshot) = snapshot.as_mut() {
|
if let Some(snapshot) = snapshot.as_mut() {
|
||||||
|
|||||||
6
test-projects/malformed-stuff/default.project.json
Normal file
6
test-projects/malformed-stuff/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "malformed-stuff",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
2
test-projects/malformed-stuff/src/bad-model.model.json
Normal file
2
test-projects/malformed-stuff/src/bad-model.model.json
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ahhh this isn't a JSON model
|
||||||
|
bamboozled again
|
||||||
@@ -11,10 +11,7 @@
|
|||||||
"HttpService": {
|
"HttpService": {
|
||||||
"$className": "HttpService",
|
"$className": "HttpService",
|
||||||
"$properties": {
|
"$properties": {
|
||||||
"HttpEnabled": {
|
"HttpEnabled": true
|
||||||
"Type": "Bool",
|
|
||||||
"Value": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user