Compare commits

...

37 Commits

Author SHA1 Message Date
Lucien Greathouse
a86001b85c Release 0.4.10 2018-06-01 23:51:35 -07:00
Lucien Greathouse
d6dd46c467 Fix JsonModelPlugin marking paths as changed correctly 2018-06-01 23:38:49 -07:00
Lucien Greathouse
320974074c Update docs 2018-06-01 23:33:36 -07:00
Lucien Greathouse
7b824abe52 Update CHANGES 2018-06-01 23:30:59 -07:00
Lucien Greathouse
bfd33f4b8d Support init.model.json
Closes #66.
2018-06-01 23:29:39 -07:00
Lucien Greathouse
d5a21a0513 Update plugin .luacheckrc to be more strict 2018-06-01 23:11:58 -07:00
Lucien Greathouse
c894b38f06 Improve plugin API robustness 2018-06-01 23:11:50 -07:00
Lucien Greathouse
a86347ea32 Add typechecks to reconciler and improve robustness a touch 2018-06-01 22:34:11 -07:00
Lucien Greathouse
b60bfc7495 Make nil checks more robust.
This represents an evolution in how I've been thinking about Lua -- using boolean coercion
is generally a bad idea I think because it obscures the underlying types.

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 B

View File

@@ -26,13 +26,21 @@ Will turn into these instances in Roblox:
![Example of Roblox instances](/images/sync-example.png)
## Models
Rojo supports a JSON model format for representing simple models. It's designed for instance types like `BindableEvent` or `*Value` objects, and is not suitable for larger models.
Rojo supports a JSON model format for representing simple models. It's designed for instance types like `BindableEvent` or `Value` objects, and is not suitable for larger models.
Rojo JSON models are stored in `.model.json` files.
Starting in Rojo version **0.4.10**, model files named `init.model.json` that are located in folders will replace that folder, much like Rojo's `init.lua` support. This can be useful to version instances like `Tool` that tend to contain several instances as well as one or more scripts.
!!! info
In the future, Rojo will support `.rbxmx` models. See [issue #7](https://github.com/LPGhatguy/rojo/issues/7) for more details and updates on this feature.
JSON model files are strict, with every property being required. They look like this:
!!! warning
Prior to Rojo version **0.4.9**, the `Properties` and `Children` properties are required on all instances in JSON models!
JSON model files are fairly strict; any syntax errors will cause the model to fail to sync! They look like this:
`hello.model.json`
```json
{
"Name": "hello",
@@ -40,14 +48,11 @@ JSON model files are strict, with every property being required. They look like
"Children": [
{
"Name": "Some Part",
"ClassName": "Part",
"Children": [],
"Properties": {}
"ClassName": "Part"
},
{
"Name": "Some StringValue",
"ClassName": "StringValue",
"Children": [],
"Properties": {
"Value": {
"Type": "String",
@@ -55,7 +60,6 @@ JSON model files are strict, with every property being required. They look like
}
}
}
],
"Properties": {}
]
}
```

View File

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

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ local function main()
local toolbar = plugin:CreateToolbar("Rojo Plugin " .. displayedVersion)
toolbar:CreateButton("Test Connection", "Connect to Rojo Server", "")
toolbar:CreateButton("Test Connection", "Connect to Rojo Server", Config.icons.testConnection)
.Click:Connect(function()
checkUpgrade()
@@ -55,7 +55,7 @@ local function main()
end)
end)
toolbar:CreateButton("Sync In", "Sync into Roblox Studio", "")
toolbar:CreateButton("Sync In", "Sync into Roblox Studio", Config.icons.syncIn)
.Click:Connect(function()
checkUpgrade()
@@ -65,7 +65,7 @@ local function main()
end)
end)
toolbar:CreateButton("Toggle Polling", "Poll server for changes", "")
toolbar:CreateButton("Toggle Polling", "Poll server for changes", Config.icons.togglePolling)
.Click:Connect(function()
checkUpgrade()

View File

@@ -1,9 +1,15 @@
local CoreGui = game:GetService("CoreGui")
local Promise = require(script.Parent.Parent.modules.Promise)
local Config = require(script.Parent.Config)
local Http = require(script.Parent.Http)
local Api = require(script.Parent.Api)
local Reconciler = require(script.Parent.Reconciler)
local Version = require(script.Parent.Version)
local MESSAGE_SERVER_CHANGED = "Rojo: The server has changed since the last request, reloading plugin..."
local MESSAGE_PLUGIN_CHANGED = "Rojo: Another instance of Rojo came online, unloading..."
local function collectMatch(source, pattern)
local result = {}
@@ -35,9 +41,23 @@ function Plugin.new()
setmetatable(self, Plugin)
do
local uiName = ("Rojo %s UI"):format(Version.display(Config.version))
if Config.dev then
uiName = "Rojo Dev UI"
end
-- If there's an existing Rojo UI, like from a Roblox plugin upgrade
-- that wasn't Rojo, make sure we clean it up.
local existingUi = CoreGui:FindFirstChild(uiName)
if existingUi ~= nil then
existingUi:Destroy()
end
local screenGui = Instance.new("ScreenGui")
screenGui.Name = "Rojo UI"
screenGui.Parent = game.CoreGui
screenGui.Name = uiName
screenGui.Parent = CoreGui
screenGui.DisplayOrder = -1
screenGui.Enabled = false
@@ -54,6 +74,19 @@ function Plugin.new()
label.Parent = screenGui
self._label = screenGui
-- If our UI was destroyed, we assume it was from another instance of
-- the Rojo plugin coming online.
--
-- Roblox doesn't notify plugins when they get unloaded, so this is the
-- best trigger we have right now unless we create a dedicated event
-- object.
screenGui.AncestryChanged:Connect(function(_, parent)
if parent == nil then
warn(MESSAGE_PLUGIN_CHANGED)
self:restart()
end
end)
end
return self
@@ -64,30 +97,35 @@ end
restarted.
]]
function Plugin:restart()
warn("Rojo: The server has changed since the last request, reloading plugin...")
self:stopPolling()
self._reconciler:destruct()
self._reconciler = Reconciler.new()
self._reconciler:clear()
self._api = nil
self._polling = false
self._syncInProgress = false
end
function Plugin:api()
if not self._api then
self._api = Api.connect(self._http)
:catch(function(err)
self._api = nil
function Plugin:getApi()
if self._api == nil then
return Api.connect(self._http)
:andThen(function(api)
self._api = api
return api
end, function(err)
return Promise.reject(err)
end)
end
return self._api
return Promise.resolve(self._api)
end
function Plugin:connect()
print("Rojo: Testing connection...")
return self:api()
return self:getApi()
:andThen(function(api)
local ok, info = api:getInfo():await()
@@ -101,6 +139,7 @@ function Plugin:connect()
end)
:catch(function(err)
if err == Api.Error.ServerIdMismatch then
warn(MESSAGE_SERVER_CHANGED)
self:restart()
return self:connect()
else
@@ -122,7 +161,7 @@ function Plugin:stopPolling()
return Promise.resolve(false)
end
print("Rojo Stopped polling server for changes.")
print("Rojo: Stopped polling server for changes.")
self._polling = false
self._label.Enabled = false
@@ -171,25 +210,25 @@ function Plugin:startPolling()
return
end
print("Rojo: Polling server for changes...")
print("Rojo: Starting to poll server for changes...")
self._polling = true
self._label.Enabled = true
return self:api()
return self:getApi()
:andThen(function(api)
local syncOk, result = self:syncIn():await()
if not syncOk then
return Promise.reject(result)
end
local infoOk, info = api:getInfo():await()
if not infoOk then
return Promise.reject(info)
end
local syncOk, result = self:syncIn():await()
if not syncOk then
return Promise.reject(result)
end
while self._polling do
local changesOk, changes = api:getChanges():await()
@@ -215,12 +254,12 @@ function Plugin:startPolling()
end
end)
:catch(function(err)
self:stopPolling()
if err == Api.Error.ServerIdMismatch then
warn(MESSAGE_SERVER_CHANGED)
self:restart()
return self:startPolling()
else
self:stopPolling()
return Promise.reject(err)
end
end)
@@ -236,7 +275,7 @@ function Plugin:syncIn()
self._syncInProgress = true
print("Rojo: Syncing from server...")
return self:api()
return self:getApi()
:andThen(function(api)
local ok, info = api:getInfo():await()
@@ -259,6 +298,7 @@ function Plugin:syncIn()
self._syncInProgress = false
if err == Api.Error.ServerIdMismatch then
warn(MESSAGE_SERVER_CHANGED)
self:restart()
return self:syncIn()
else

View File

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

View File

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

2
server/Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ use std::collections::HashMap;
use regex::Regex;
use core::Route;
use plugin::{Plugin, PluginChain, TransformFileResult, RbxChangeResult, FileChangeResult};
use plugin::{Plugin, PluginChain, TransformFileResult, FileChangeResult};
use rbx::{RbxInstance, RbxValue};
use vfs::VfsItem;
@@ -117,8 +117,4 @@ impl Plugin for ScriptPlugin {
FileChangeResult::Pass
}
}
fn handle_rbx_change(&self, _route: &Route, _rbx_item: &RbxInstance) -> RbxChangeResult {
RbxChangeResult::Pass
}
}

View File

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

View File

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

View File

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

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

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

View File

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