mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Stomp out old plugin, begin foundations for v2 plugin
This commit is contained in:
parent
de02966db6
commit
3e4b595e3b
9 changed files with 97 additions and 738 deletions
|
|
@ -1,112 +0,0 @@
|
||||||
local HttpService = game:GetService("HttpService")
|
|
||||||
|
|
||||||
local Config = require(script.Parent.Config)
|
|
||||||
local Promise = require(script.Parent.Promise)
|
|
||||||
local Version = require(script.Parent.Version)
|
|
||||||
|
|
||||||
local Api = {}
|
|
||||||
Api.__index = Api
|
|
||||||
|
|
||||||
Api.Error = {
|
|
||||||
ServerIdMismatch = "ServerIdMismatch",
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(Api.Error, {
|
|
||||||
__index = function(_, key)
|
|
||||||
error("Invalid API.Error name " .. key, 2)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Api.connect(Http) -> Promise<Api>
|
|
||||||
|
|
||||||
Create a new Api using the given HTTP implementation.
|
|
||||||
|
|
||||||
Attempting to invoke methods on an invalid conext will throw errors!
|
|
||||||
]]
|
|
||||||
function Api.connect(http)
|
|
||||||
local context = {
|
|
||||||
http = http,
|
|
||||||
serverId = nil,
|
|
||||||
currentTime = 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(context, Api)
|
|
||||||
|
|
||||||
return context:_start()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Api:_start()
|
|
||||||
return self.http:get("/")
|
|
||||||
:andThen(function(response)
|
|
||||||
response = response:json()
|
|
||||||
|
|
||||||
if response.protocolVersion ~= Config.protocolVersion then
|
|
||||||
local message = (
|
|
||||||
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible." ..
|
|
||||||
"\nMake sure you have matching versions of both the Rojo plugin and server!" ..
|
|
||||||
"\n\nYour client is version %s, with protocol version %s. It expects server version %s." ..
|
|
||||||
"\nYour server is version %s, with protocol version %s." ..
|
|
||||||
"\n\nGo to https://github.com/LPGhatguy/rojo for more details."
|
|
||||||
):format(
|
|
||||||
Version.display(Config.version), Config.protocolVersion,
|
|
||||||
Config.expectedApiVersionString,
|
|
||||||
response.serverVersion, response.protocolVersion
|
|
||||||
)
|
|
||||||
|
|
||||||
return Promise.reject(message)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.serverId = response.serverId
|
|
||||||
self.currentTime = response.currentTime
|
|
||||||
|
|
||||||
return self
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Api:getInfo()
|
|
||||||
return self.http:get("/")
|
|
||||||
:andThen(function(response)
|
|
||||||
response = response:json()
|
|
||||||
|
|
||||||
if response.serverId ~= self.serverId then
|
|
||||||
return Promise.reject(Api.Error.ServerIdMismatch)
|
|
||||||
end
|
|
||||||
|
|
||||||
return response
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Api:read(paths)
|
|
||||||
local body = HttpService:JSONEncode(paths)
|
|
||||||
|
|
||||||
return self.http:post("/read", body)
|
|
||||||
:andThen(function(response)
|
|
||||||
response = response:json()
|
|
||||||
|
|
||||||
if response.serverId ~= self.serverId then
|
|
||||||
return Promise.reject(Api.Error.ServerIdMismatch)
|
|
||||||
end
|
|
||||||
|
|
||||||
return response.items
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Api:getChanges()
|
|
||||||
local url = ("/changes/%f"):format(self.currentTime)
|
|
||||||
|
|
||||||
return self.http:get(url)
|
|
||||||
:andThen(function(response)
|
|
||||||
response = response:json()
|
|
||||||
|
|
||||||
if response.serverId ~= self.serverId then
|
|
||||||
return Promise.reject(Api.Error.ServerIdMismatch)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.currentTime = response.currentTime
|
|
||||||
|
|
||||||
return response.changes
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Api
|
|
||||||
63
plugin/src/ApiContext.lua
Normal file
63
plugin/src/ApiContext.lua
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
|
local Config = require(script.Parent.Config)
|
||||||
|
local Promise = require(script.Parent.Promise)
|
||||||
|
local Version = require(script.Parent.Version)
|
||||||
|
local Http = require(script.Parent.Http)
|
||||||
|
|
||||||
|
local ApiContext = {}
|
||||||
|
ApiContext.__index = ApiContext
|
||||||
|
|
||||||
|
ApiContext.Error = {
|
||||||
|
ServerIdMismatch = "ServerIdMismatch",
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(ApiContext.Error, {
|
||||||
|
__index = function(_, key)
|
||||||
|
error("Invalid API.Error name " .. key, 2)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
function ApiContext.new(url, onMessage)
|
||||||
|
assert(type(url) == "string")
|
||||||
|
assert(type(onMessage) == "function")
|
||||||
|
|
||||||
|
local context = {
|
||||||
|
url = url,
|
||||||
|
onMessage = onMessage,
|
||||||
|
serverId = nil,
|
||||||
|
currentTime = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(context, ApiContext)
|
||||||
|
|
||||||
|
return context
|
||||||
|
end
|
||||||
|
|
||||||
|
function ApiContext:connect()
|
||||||
|
return Http.get(self.url)
|
||||||
|
:andThen(function(response)
|
||||||
|
response = response:json()
|
||||||
|
|
||||||
|
if response.protocolVersion ~= Config.protocolVersion then
|
||||||
|
local message = (
|
||||||
|
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible." ..
|
||||||
|
"\nMake sure you have matching versions of both the Rojo plugin and server!" ..
|
||||||
|
"\n\nYour client is version %s, with protocol version %s. It expects server version %s." ..
|
||||||
|
"\nYour server is version %s, with protocol version %s." ..
|
||||||
|
"\n\nGo to https://github.com/LPGhatguy/rojo for more details."
|
||||||
|
):format(
|
||||||
|
Version.display(Config.version), Config.protocolVersion,
|
||||||
|
Config.expectedApiContextVersionString,
|
||||||
|
response.serverVersion, response.protocolVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
return Promise.reject(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
self.serverId = response.serverId
|
||||||
|
self.currentTime = response.currentTime
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ApiContext
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
return {
|
return {
|
||||||
pollingRate = 0.2,
|
version = {0, 5, 0},
|
||||||
version = {0, 4, 4},
|
expectedServerVersionString = "0.5.x",
|
||||||
expectedServerVersionString = "0.4.x",
|
protocolVersion = 2,
|
||||||
protocolVersion = 1,
|
port = 34872,
|
||||||
dev = false,
|
dev = false,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,26 +13,13 @@ local function dprint(...)
|
||||||
end
|
end
|
||||||
|
|
||||||
local Http = {}
|
local Http = {}
|
||||||
Http.__index = Http
|
|
||||||
|
|
||||||
function Http.new(baseUrl)
|
function Http.get(url)
|
||||||
assert(type(baseUrl) == "string", "Http.new needs a baseUrl!")
|
dprint("\nGET", url)
|
||||||
|
|
||||||
local http = {
|
|
||||||
baseUrl = baseUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(http, Http)
|
|
||||||
|
|
||||||
return http
|
|
||||||
end
|
|
||||||
|
|
||||||
function Http:get(endpoint)
|
|
||||||
dprint("\nGET", endpoint)
|
|
||||||
return Promise.new(function(resolve, reject)
|
return Promise.new(function(resolve, reject)
|
||||||
spawn(function()
|
spawn(function()
|
||||||
local ok, result = pcall(function()
|
local ok, result = pcall(function()
|
||||||
return HttpService:GetAsync(self.baseUrl .. endpoint, true)
|
return HttpService:GetAsync(url, true)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if ok then
|
if ok then
|
||||||
|
|
@ -45,13 +32,13 @@ function Http:get(endpoint)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Http:post(endpoint, body)
|
function Http.post(url, body)
|
||||||
dprint("\nPOST", endpoint)
|
dprint("\nPOST", url)
|
||||||
dprint(body)
|
dprint(body)
|
||||||
return Promise.new(function(resolve, reject)
|
return Promise.new(function(resolve, reject)
|
||||||
spawn(function()
|
spawn(function()
|
||||||
local ok, result = pcall(function()
|
local ok, result = pcall(function()
|
||||||
return HttpService:PostAsync(self.baseUrl .. endpoint, body)
|
return HttpService:PostAsync(url, body)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if ok then
|
if ok then
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ if not plugin then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local Plugin = require(script.Parent.Plugin)
|
local Session = require(script.Parent.Session)
|
||||||
local Config = require(script.Parent.Config)
|
local Config = require(script.Parent.Config)
|
||||||
local Version = require(script.Parent.Version)
|
local Version = require(script.Parent.Version)
|
||||||
|
|
||||||
|
|
@ -39,40 +39,16 @@ local function checkUpgrade()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function main()
|
local function main()
|
||||||
local pluginInstance = Plugin.new()
|
|
||||||
|
|
||||||
local displayedVersion = Config.dev and "DEV" or Version.display(Config.version)
|
local displayedVersion = Config.dev and "DEV" or Version.display(Config.version)
|
||||||
|
|
||||||
local toolbar = plugin:CreateToolbar("Rojo Plugin " .. displayedVersion)
|
local toolbar = plugin:CreateToolbar("Rojo " .. displayedVersion)
|
||||||
|
|
||||||
toolbar:CreateButton("Test Connection", "Connect to Rojo Server", "")
|
toolbar:CreateButton("Connect", "Connect to Rojo Session", "")
|
||||||
.Click:Connect(function()
|
.Click:Connect(function()
|
||||||
checkUpgrade()
|
checkUpgrade()
|
||||||
|
Session.new()
|
||||||
|
|
||||||
pluginInstance:connect()
|
error("NYI")
|
||||||
:catch(function(err)
|
|
||||||
warn(err)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
toolbar:CreateButton("Sync In", "Sync into Roblox Studio", "")
|
|
||||||
.Click:Connect(function()
|
|
||||||
checkUpgrade()
|
|
||||||
|
|
||||||
pluginInstance:syncIn()
|
|
||||||
:catch(function(err)
|
|
||||||
warn(err)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
toolbar:CreateButton("Toggle Polling", "Poll server for changes", "")
|
|
||||||
.Click:Connect(function()
|
|
||||||
checkUpgrade()
|
|
||||||
|
|
||||||
pluginInstance:togglePolling()
|
|
||||||
:catch(function(err)
|
|
||||||
warn(err)
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,257 +0,0 @@
|
||||||
local Config = require(script.Parent.Config)
|
|
||||||
local Http = require(script.Parent.Http)
|
|
||||||
local Api = require(script.Parent.Api)
|
|
||||||
local Promise = require(script.Parent.Promise)
|
|
||||||
local Reconciler = require(script.Parent.Reconciler)
|
|
||||||
|
|
||||||
local function collectMatch(source, pattern)
|
|
||||||
local result = {}
|
|
||||||
|
|
||||||
for match in source:gmatch(pattern) do
|
|
||||||
table.insert(result, match)
|
|
||||||
end
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
local Plugin = {}
|
|
||||||
Plugin.__index = Plugin
|
|
||||||
|
|
||||||
function Plugin.new()
|
|
||||||
local address = "localhost"
|
|
||||||
local port = Config.dev and 8001 or 8000
|
|
||||||
|
|
||||||
local remote = ("http://%s:%d"):format(address, port)
|
|
||||||
|
|
||||||
local self = {
|
|
||||||
_http = Http.new(remote),
|
|
||||||
_reconciler = Reconciler.new(),
|
|
||||||
_api = nil,
|
|
||||||
_polling = false,
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(self, Plugin)
|
|
||||||
|
|
||||||
do
|
|
||||||
local screenGui = Instance.new("ScreenGui")
|
|
||||||
screenGui.Name = "Rojo UI"
|
|
||||||
screenGui.Parent = game.CoreGui
|
|
||||||
screenGui.DisplayOrder = -1
|
|
||||||
screenGui.Enabled = false
|
|
||||||
|
|
||||||
local label = Instance.new("TextLabel")
|
|
||||||
label.Font = Enum.Font.SourceSans
|
|
||||||
label.TextSize = 20
|
|
||||||
label.Text = "Rojo polling..."
|
|
||||||
label.BackgroundColor3 = Color3.fromRGB(31, 31, 31)
|
|
||||||
label.BackgroundTransparency = 0.5
|
|
||||||
label.BorderSizePixel = 0
|
|
||||||
label.TextColor3 = Color3.new(1, 1, 1)
|
|
||||||
label.Size = UDim2.new(0, 120, 0, 28)
|
|
||||||
label.Position = UDim2.new(0, 0, 0, 0)
|
|
||||||
label.Parent = screenGui
|
|
||||||
|
|
||||||
self._label = screenGui
|
|
||||||
end
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Clears all state and issues a notice to the user that the plugin has
|
|
||||||
restarted.
|
|
||||||
]]
|
|
||||||
function Plugin:restart()
|
|
||||||
warn("The server has changed since the last request, reloading plugin...")
|
|
||||||
|
|
||||||
self._reconciler:clear()
|
|
||||||
self._api = nil
|
|
||||||
self._polling = false
|
|
||||||
end
|
|
||||||
|
|
||||||
function Plugin:api()
|
|
||||||
if not self._api then
|
|
||||||
self._api = Api.connect(self._http)
|
|
||||||
:catch(function(err)
|
|
||||||
self._api = nil
|
|
||||||
return Promise.reject(err)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return self._api
|
|
||||||
end
|
|
||||||
|
|
||||||
function Plugin:connect()
|
|
||||||
print("Testing connection...")
|
|
||||||
|
|
||||||
return self:api()
|
|
||||||
:andThen(function(api)
|
|
||||||
local ok, info = api:getInfo():await()
|
|
||||||
|
|
||||||
if not ok then
|
|
||||||
return Promise.reject(info)
|
|
||||||
end
|
|
||||||
|
|
||||||
print("Server found!")
|
|
||||||
print("Protocol version:", info.protocolVersion)
|
|
||||||
print("Server version:", info.serverVersion)
|
|
||||||
end)
|
|
||||||
:catch(function(err)
|
|
||||||
if err == Api.Error.ServerIdMismatch then
|
|
||||||
self:restart()
|
|
||||||
return self:connect()
|
|
||||||
else
|
|
||||||
return Promise.reject(err)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Plugin:togglePolling()
|
|
||||||
if self._polling then
|
|
||||||
return self:stopPolling()
|
|
||||||
else
|
|
||||||
return self:startPolling()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Plugin:stopPolling()
|
|
||||||
if not self._polling then
|
|
||||||
return Promise.resolve(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
print("Stopped polling.")
|
|
||||||
|
|
||||||
self._polling = false
|
|
||||||
self._label.Enabled = false
|
|
||||||
|
|
||||||
return Promise.resolve(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Plugin:_pull(api, project, routes)
|
|
||||||
return api:read(routes)
|
|
||||||
:andThen(function(items)
|
|
||||||
for index = 1, #routes do
|
|
||||||
local itemRoute = routes[index]
|
|
||||||
local partitionName = itemRoute[1]
|
|
||||||
local partition = project.partitions[partitionName]
|
|
||||||
local item = items[index]
|
|
||||||
|
|
||||||
local partitionRoute = collectMatch(partition.target, "[^.]+")
|
|
||||||
|
|
||||||
-- If the item route's length was 1, we need to rename the instance to
|
|
||||||
-- line up with the partition's root object name.
|
|
||||||
--
|
|
||||||
-- This is a HACK!
|
|
||||||
if #itemRoute == 1 then
|
|
||||||
if item then
|
|
||||||
local objectName = partition.target:match("[^.]+$")
|
|
||||||
item.Name = objectName
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local fullRoute = {}
|
|
||||||
for _, piece in ipairs(partitionRoute) do
|
|
||||||
table.insert(fullRoute, piece)
|
|
||||||
end
|
|
||||||
|
|
||||||
for i = 2, #itemRoute do
|
|
||||||
table.insert(fullRoute, itemRoute[i])
|
|
||||||
end
|
|
||||||
|
|
||||||
self._reconciler:reconcileRoute(fullRoute, item, itemRoute)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Plugin:startPolling()
|
|
||||||
if self._polling then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
print("Starting to poll...")
|
|
||||||
|
|
||||||
self._polling = true
|
|
||||||
self._label.Enabled = true
|
|
||||||
|
|
||||||
return self:api()
|
|
||||||
:andThen(function(api)
|
|
||||||
local syncOk, result = self:syncIn():await()
|
|
||||||
|
|
||||||
if not syncOk then
|
|
||||||
return Promise.reject(result)
|
|
||||||
end
|
|
||||||
|
|
||||||
local infoOk, info = api:getInfo():await()
|
|
||||||
|
|
||||||
if not infoOk then
|
|
||||||
return Promise.reject(info)
|
|
||||||
end
|
|
||||||
|
|
||||||
while self._polling do
|
|
||||||
local changesOk, changes = api:getChanges():await()
|
|
||||||
|
|
||||||
if not changesOk then
|
|
||||||
return Promise.reject(changes)
|
|
||||||
end
|
|
||||||
|
|
||||||
if #changes > 0 then
|
|
||||||
local routes = {}
|
|
||||||
|
|
||||||
for _, change in ipairs(changes) do
|
|
||||||
table.insert(routes, change.route)
|
|
||||||
end
|
|
||||||
|
|
||||||
local pullOk, pullResult = self:_pull(api, info.project, routes):await()
|
|
||||||
|
|
||||||
if not pullOk then
|
|
||||||
return Promise.reject(pullResult)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
wait(Config.pollingRate)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
:catch(function(err)
|
|
||||||
self:stopPolling()
|
|
||||||
|
|
||||||
if err == Api.Error.ServerIdMismatch then
|
|
||||||
self:restart()
|
|
||||||
return self:startPolling()
|
|
||||||
else
|
|
||||||
return Promise.reject(err)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Plugin:syncIn()
|
|
||||||
print("Syncing from server...")
|
|
||||||
|
|
||||||
return self:api()
|
|
||||||
:andThen(function(api)
|
|
||||||
local ok, info = api:getInfo():await()
|
|
||||||
|
|
||||||
if not ok then
|
|
||||||
return Promise.reject(info)
|
|
||||||
end
|
|
||||||
|
|
||||||
local routes = {}
|
|
||||||
|
|
||||||
for name in pairs(info.project.partitions) do
|
|
||||||
table.insert(routes, {name})
|
|
||||||
end
|
|
||||||
|
|
||||||
self:_pull(api, info.project, routes)
|
|
||||||
|
|
||||||
print("Sync successful!")
|
|
||||||
end)
|
|
||||||
:catch(function(err)
|
|
||||||
if err == Api.Error.ServerIdMismatch then
|
|
||||||
self:restart()
|
|
||||||
return self:syncIn()
|
|
||||||
else
|
|
||||||
return Promise.reject(err)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Plugin
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
local RouteMap = require(script.Parent.RouteMap)
|
|
||||||
|
|
||||||
local function classEqual(rbx, className)
|
|
||||||
if className == "*" then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
return rbx.ClassName == className
|
|
||||||
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)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Attempts to match up Roblox instances and object specifiers for
|
|
||||||
reconciliation.
|
|
||||||
|
|
||||||
An object is considered a match if they have the same Name and ClassName.
|
|
||||||
|
|
||||||
primaryChildren and secondaryChildren can each be either a list of Roblox
|
|
||||||
instances or object specifiers. Since they share a common shape, switching
|
|
||||||
the two around isn't problematic!
|
|
||||||
|
|
||||||
visited is expected to be an empty table initially. It will be filled with
|
|
||||||
the set of children that have been visited so far.
|
|
||||||
]]
|
|
||||||
local function findNextChildPair(primaryChildren, secondaryChildren, visited)
|
|
||||||
for _, primaryChild in ipairs(primaryChildren) do
|
|
||||||
if not visited[primaryChild] then
|
|
||||||
visited[primaryChild] = true
|
|
||||||
|
|
||||||
for _, secondaryChild in ipairs(secondaryChildren) do
|
|
||||||
if primaryChild.ClassName == secondaryChild.ClassName and primaryChild.Name == secondaryChild.Name then
|
|
||||||
visited[secondaryChild] = true
|
|
||||||
|
|
||||||
return primaryChild, secondaryChild
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return primaryChild, nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local Reconciler = {}
|
|
||||||
Reconciler.__index = Reconciler
|
|
||||||
|
|
||||||
function Reconciler.new()
|
|
||||||
local reconciler = {
|
|
||||||
_routeMap = RouteMap.new(),
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(reconciler, Reconciler)
|
|
||||||
|
|
||||||
return reconciler
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
A semi-smart algorithm that attempts to apply the given item's children to
|
|
||||||
an existing Roblox object.
|
|
||||||
]]
|
|
||||||
function Reconciler:_reconcileChildren(rbx, item)
|
|
||||||
local visited = {}
|
|
||||||
local rbxChildren = rbx:GetChildren()
|
|
||||||
|
|
||||||
-- Reconcile any children that were added or updated
|
|
||||||
while true do
|
|
||||||
local itemChild, rbxChild = findNextChildPair(item.Children, rbxChildren, visited)
|
|
||||||
|
|
||||||
if not itemChild then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
reparent(self:reconcile(rbxChild, itemChild), rbx)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Reconcile any children that were deleted
|
|
||||||
while true do
|
|
||||||
local rbxChild, itemChild = findNextChildPair(rbxChildren, item.Children, visited)
|
|
||||||
|
|
||||||
if not rbxChild then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
reparent(self:reconcile(rbxChild, itemChild), rbx)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Construct a new Roblox object from the given item.
|
|
||||||
]]
|
|
||||||
function Reconciler:_reify(item)
|
|
||||||
local className = item.ClassName
|
|
||||||
|
|
||||||
-- "*" represents a match of any class. It reifies as a folder!
|
|
||||||
if className == "*" then
|
|
||||||
className = "Folder"
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
self:_reconcileChildren(rbx, item)
|
|
||||||
|
|
||||||
if item.Route then
|
|
||||||
self._routeMap:insert(item.Route, rbx)
|
|
||||||
end
|
|
||||||
|
|
||||||
return rbx
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Clears any state that the Reconciler has, effectively restarting it.
|
|
||||||
]]
|
|
||||||
function Reconciler:clear()
|
|
||||||
self._routeMap:clear()
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Apply the changes represented by the given item to a Roblox object that's a
|
|
||||||
child of the given instance.
|
|
||||||
]]
|
|
||||||
function Reconciler:reconcile(rbx, item)
|
|
||||||
-- Item was deleted
|
|
||||||
if not item then
|
|
||||||
if rbx then
|
|
||||||
rbx:Destroy()
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Item was created!
|
|
||||||
if not rbx then
|
|
||||||
return self:_reify(item)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Item changed type!
|
|
||||||
if not classEqual(rbx, item.ClassName) then
|
|
||||||
rbx:Destroy()
|
|
||||||
|
|
||||||
rbx = 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
|
|
||||||
self:_reconcileChildren(rbx, item)
|
|
||||||
|
|
||||||
if item.Route then
|
|
||||||
self._routeMap:insert(item.Route, rbx)
|
|
||||||
end
|
|
||||||
|
|
||||||
return rbx
|
|
||||||
end
|
|
||||||
|
|
||||||
function Reconciler:reconcileRoute(route, item, itemRoute)
|
|
||||||
local parent
|
|
||||||
local rbx = game
|
|
||||||
|
|
||||||
for i = 1, #route do
|
|
||||||
local piece = route[i]
|
|
||||||
|
|
||||||
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)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- We don't want to create a folder if we're reaching our target item!
|
|
||||||
if not child and i ~= #route then
|
|
||||||
child = Instance.new("Folder")
|
|
||||||
child.Parent = rbx
|
|
||||||
child.Name = piece
|
|
||||||
end
|
|
||||||
|
|
||||||
parent = rbx
|
|
||||||
rbx = child
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Let's check the route map!
|
|
||||||
if not rbx then
|
|
||||||
rbx = self._routeMap:get(itemRoute)
|
|
||||||
end
|
|
||||||
|
|
||||||
rbx = self:reconcile(rbx, item)
|
|
||||||
|
|
||||||
reparent(rbx, parent)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Reconciler
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
--[[
|
|
||||||
A map from Route objects (given by the server) to Roblox instances (created
|
|
||||||
by the plugin).
|
|
||||||
]]
|
|
||||||
|
|
||||||
local function hashRoute(route)
|
|
||||||
return table.concat(route, "/")
|
|
||||||
end
|
|
||||||
|
|
||||||
local RouteMap = {}
|
|
||||||
RouteMap.__index = RouteMap
|
|
||||||
|
|
||||||
function RouteMap.new()
|
|
||||||
local self = {
|
|
||||||
_map = {},
|
|
||||||
_reverseMap = {},
|
|
||||||
_connectionsByRbx = {},
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(self, RouteMap)
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
function RouteMap:insert(route, rbx)
|
|
||||||
local hashed = hashRoute(route)
|
|
||||||
|
|
||||||
self._map[hashed] = rbx
|
|
||||||
self._reverseMap[rbx] = hashed
|
|
||||||
self._connectionsByRbx[rbx] = rbx.AncestryChanged:Connect(function(_, parent)
|
|
||||||
if parent == nil then
|
|
||||||
self:removeByRbx(rbx)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function RouteMap:get(route)
|
|
||||||
return self._map[hashRoute(route)]
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function RouteMap:removeRbxDescendants(parentRbx)
|
|
||||||
for rbx in pairs(self._reverseMap) do
|
|
||||||
if rbx:IsDescendantOf(parentRbx) then
|
|
||||||
self:removeByRbx(rbx)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function RouteMap:clear()
|
|
||||||
self._map = {}
|
|
||||||
self._reverseMap = {}
|
|
||||||
|
|
||||||
for _, connection in pairs(self._connectionsByRbx) do
|
|
||||||
connection:Disconnect()
|
|
||||||
end
|
|
||||||
|
|
||||||
self._connectionsByRbx = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
function RouteMap:visualize()
|
|
||||||
-- Log all of our keys so that the visualization has a stable order.
|
|
||||||
local keys = {}
|
|
||||||
|
|
||||||
for key in pairs(self._map) do
|
|
||||||
table.insert(keys, key)
|
|
||||||
end
|
|
||||||
|
|
||||||
table.sort(keys)
|
|
||||||
|
|
||||||
local buffer = {}
|
|
||||||
for _, key in ipairs(keys) do
|
|
||||||
local visualized = ("- %s: %s"):format(
|
|
||||||
key,
|
|
||||||
self._map[key]:GetFullName()
|
|
||||||
)
|
|
||||||
table.insert(buffer, visualized)
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.concat(buffer, "\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
return RouteMap
|
|
||||||
19
plugin/src/Session.lua
Normal file
19
plugin/src/Session.lua
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
local Config = require(script.Parent.Config)
|
||||||
|
local Http = require(script.Parent.Http)
|
||||||
|
local ApiContext = require(script.Parent.ApiContext)
|
||||||
|
local Promise = require(script.Parent.Promise)
|
||||||
|
|
||||||
|
local REMOTE_URL = ("http://localhost:%d"):format(Config.port)
|
||||||
|
|
||||||
|
local Session = {}
|
||||||
|
Session.__index = Session
|
||||||
|
|
||||||
|
function Session.new()
|
||||||
|
local self = {}
|
||||||
|
|
||||||
|
setmetatable(self, Session)
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
return Session
|
||||||
Loading…
Add table
Add a link
Reference in a new issue