forked from rojo-rbx/rojo
312 lines
6.9 KiB
Lua
312 lines
6.9 KiB
Lua
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 = {}
|
|
|
|
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,
|
|
_syncInProgress = false,
|
|
}
|
|
|
|
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 = uiName
|
|
screenGui.Parent = 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
|
|
|
|
-- 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
|
|
end
|
|
|
|
--[[
|
|
Clears all state and issues a notice to the user that the plugin has
|
|
restarted.
|
|
]]
|
|
function Plugin:restart()
|
|
self:stopPolling()
|
|
|
|
self._reconciler:destruct()
|
|
self._reconciler = Reconciler.new()
|
|
|
|
self._api = nil
|
|
self._polling = false
|
|
self._syncInProgress = false
|
|
end
|
|
|
|
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 Promise.resolve(self._api)
|
|
end
|
|
|
|
function Plugin:connect()
|
|
print("Rojo: Testing connection...")
|
|
|
|
return self:getApi()
|
|
:andThen(function(api)
|
|
local ok, info = api:getInfo():await()
|
|
|
|
if not ok then
|
|
return Promise.reject(info)
|
|
end
|
|
|
|
print("Rojo: Server found!")
|
|
print("Rojo: Protocol version:", info.protocolVersion)
|
|
print("Rojo: Server version:", info.serverVersion)
|
|
end)
|
|
:catch(function(err)
|
|
if err == Api.Error.ServerIdMismatch then
|
|
warn(MESSAGE_SERVER_CHANGED)
|
|
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("Rojo: Stopped polling server for changes.")
|
|
|
|
self._polling = false
|
|
self._label.Enabled = false
|
|
|
|
return Promise.resolve(true)
|
|
end
|
|
|
|
function Plugin:_pull(api, project, fileRoutes)
|
|
return api:read(fileRoutes)
|
|
:andThen(function(items)
|
|
for index = 1, #fileRoutes do
|
|
local fileRoute = fileRoutes[index]
|
|
local partitionName = fileRoute[1]
|
|
local partition = project.partitions[partitionName]
|
|
local item = items[index]
|
|
|
|
local partitionTargetRbxRoute = 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.
|
|
if item ~= nil and #fileRoute == 1 then
|
|
local objectName = partition.target:match("[^.]+$")
|
|
item.Name = objectName
|
|
end
|
|
|
|
local itemRbxRoute = {}
|
|
for _, piece in ipairs(partitionTargetRbxRoute) do
|
|
table.insert(itemRbxRoute, piece)
|
|
end
|
|
|
|
for i = 2, #fileRoute do
|
|
table.insert(itemRbxRoute, fileRoute[i])
|
|
end
|
|
|
|
self._reconciler:reconcileRoute(itemRbxRoute, item, fileRoute)
|
|
end
|
|
end)
|
|
end
|
|
|
|
function Plugin:startPolling()
|
|
if self._polling then
|
|
return
|
|
end
|
|
|
|
print("Rojo: Starting to poll server for changes...")
|
|
|
|
self._polling = true
|
|
self._label.Enabled = true
|
|
|
|
return self:getApi()
|
|
:andThen(function(api)
|
|
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()
|
|
|
|
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)
|
|
if err == Api.Error.ServerIdMismatch then
|
|
warn(MESSAGE_SERVER_CHANGED)
|
|
self:restart()
|
|
return self:startPolling()
|
|
else
|
|
self:stopPolling()
|
|
return Promise.reject(err)
|
|
end
|
|
end)
|
|
end
|
|
|
|
function Plugin:syncIn()
|
|
if self._syncInProgress then
|
|
warn("Rojo: Can't sync right now, because a sync is already in progress.")
|
|
|
|
return Promise.resolve()
|
|
end
|
|
|
|
self._syncInProgress = true
|
|
print("Rojo: Syncing from server...")
|
|
|
|
return self:getApi()
|
|
:andThen(function(api)
|
|
local ok, info = api:getInfo():await()
|
|
|
|
if not ok then
|
|
return Promise.reject(info)
|
|
end
|
|
|
|
local fileRoutes = {}
|
|
|
|
for name in pairs(info.project.partitions) do
|
|
table.insert(fileRoutes, {name})
|
|
end
|
|
|
|
local pullSuccess, pullResult = self:_pull(api, info.project, fileRoutes):await()
|
|
|
|
self._syncInProgress = false
|
|
|
|
if not pullSuccess then
|
|
return Promise.reject(pullResult)
|
|
end
|
|
|
|
print("Rojo: Sync successful!")
|
|
end)
|
|
:catch(function(err)
|
|
self._syncInProgress = false
|
|
|
|
if err == Api.Error.ServerIdMismatch then
|
|
warn(MESSAGE_SERVER_CHANGED)
|
|
self:restart()
|
|
return self:syncIn()
|
|
else
|
|
return Promise.reject(err)
|
|
end
|
|
end)
|
|
end
|
|
|
|
return Plugin
|