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