local Http = game:GetService("HttpService") local Tags = game:GetService("CollectionService") local Studio = game:GetService("StudioService") local Run = game:GetService("RunService") local Editor = game:GetService("ScriptEditorService") local History = game:GetService("ChangeHistoryService") local Tweens = game:GetService("TweenService") local Gui = game:GetService("GuiService") local API = "https://api.rosafe.net/api/v2" local VERSION = "1.2.8" local TAG = "RoSafeManagedV1" local LOGO = "rbxassetid://116249426073425" local toolbar = plugin:CreateToolbar("RoSafe") local open = toolbar:CreateButton("RoSafe", "Open RoSafe", LOGO, "RoSafe") local widget = plugin:CreateDockWidgetPluginGuiAsync("RoSafeV1", DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Right, false, false, 340, 460, 260, 280)) widget.Title = "RoSafe" local colors = { background = Color3.fromRGB(15, 16, 20), surface = Color3.fromRGB(25, 27, 33), hover = Color3.fromRGB(34, 37, 45), border = Color3.fromRGB(48, 51, 61), text = Color3.fromRGB(242, 244, 250), muted = Color3.fromRGB(153, 160, 177), blue = Color3.fromRGB(44, 91, 255), blueHover = Color3.fromRGB(68, 112, 255), green = Color3.fromRGB(112, 215, 177), red = Color3.fromRGB(255, 142, 151), } local busy, unloaded, contextReady, updateRequired = false, false, false, false local confirmingRemoval, installed, hasManaged = false, false, false local activeUniverse, activePlace, boundUser, settingsKey, savedState, credential, runtimeToken local installedVersion, operation, refreshPending, reportedHttp local HTTP_OFF = "Turn on Allow HTTP Requests in Game Settings > Security, then publish." local activeTweens, progressTween = {}, nil local function create(class, name, parent, properties) local item = Instance.new(class) item.Name = name for key, value in pairs(properties or {}) do item[key] = value end item.Parent = parent return item end local function round(item, radius) create("UICorner", "Corner", item, { CornerRadius = UDim.new(0, radius or 6) }) end local function outline(item) return create("UIStroke", "Outline", item, { Color = colors.border, Thickness = 1, ApplyStrokeMode = Enum.ApplyStrokeMode.Border }) end local function reducedMotion() local ok, value = pcall(function() return Gui.ReducedMotionEnabled end) return ok and value == true end local function animate(item, properties, duration) if activeTweens[item] then activeTweens[item]:Cancel(); activeTweens[item] = nil end if unloaded or not widget.Enabled or reducedMotion() then for key, value in pairs(properties) do item[key] = value end return end local tween = Tweens:Create(item, TweenInfo.new(duration or 0.18, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), properties) activeTweens[item] = tween tween.Completed:Connect(function() if activeTweens[item] == tween then activeTweens[item] = nil end end) tween:Play() end local root = create("ScrollingFrame", "RoSafe", widget, { Size = UDim2.fromScale(1, 1), BackgroundColor3 = colors.background, BorderSizePixel = 0, ScrollBarThickness = 3, ScrollBarImageColor3 = colors.border, AutomaticCanvasSize = Enum.AutomaticSize.Y, CanvasSize = UDim2.new(), ScrollingDirection = Enum.ScrollingDirection.Y, }) create("UIPadding", "Padding", root, { PaddingTop = UDim.new(0, 22), PaddingBottom = UDim.new(0, 22), PaddingLeft = UDim.new(0, 20), PaddingRight = UDim.new(0, 20), }) create("UIListLayout", "Layout", root, { Padding = UDim.new(0, 28), SortOrder = Enum.SortOrder.LayoutOrder }) local header = create("Frame", "Header", root, { Size = UDim2.new(1, 0, 0, 42), BackgroundTransparency = 1, LayoutOrder = 1 }) local logo = create("ImageLabel", "Logo", header, { Size = UDim2.fromOffset(40, 40), BackgroundTransparency = 1, Image = LOGO, ScaleType = Enum.ScaleType.Fit, }) round(logo, 8) local function label(name, text, parent, size) return create("TextLabel", name, parent, { Size = UDim2.new(1, 0, 0, size + 6), BackgroundTransparency = 1, Font = Enum.Font.Gotham, TextSize = size, TextColor3 = colors.text, Text = text, TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Center, RichText = false, TextWrapped = true, }) end local brand = label("Brand", "RoSafe", header, 24) brand.Position, brand.Size, brand.Font = UDim2.fromOffset(52, 5), UDim2.new(1, -108, 0, 30), Enum.Font.GothamBold local version = label("Version", "v" .. VERSION, header, 11) version.Position, version.Size = UDim2.new(1, -52, 0, 13), UDim2.fromOffset(52, 18) version.TextColor3, version.TextXAlignment = colors.muted, Enum.TextXAlignment.Right local experience = create("Frame", "Experience", root, { Size = UDim2.new(1, 0, 0, 48), BackgroundTransparency = 1, LayoutOrder = 2 }) local thumbnail = create("ImageLabel", "Thumbnail", experience, { Size = UDim2.fromOffset(48, 48), BackgroundColor3 = colors.surface, BorderSizePixel = 0, Image = "", ScaleType = Enum.ScaleType.Crop, }) round(thumbnail, 8) local gameName = label("GameName", "Current experience", experience, 14) gameName.Position, gameName.Size = UDim2.fromOffset(62, 0), UDim2.new(1, -62, 0, 48) gameName.Font, gameName.TextTruncate = Enum.Font.GothamMedium, Enum.TextTruncate.AtEnd local body = create("CanvasGroup", "Controls", root, { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = 3, }) create("UIListLayout", "Layout", body, { Padding = UDim.new(0, 14), SortOrder = Enum.SortOrder.LayoutOrder }) local heading = label("Heading", "RoSafe", body, 20) heading.LayoutOrder, heading.Font, heading.AutomaticSize = 1, Enum.Font.GothamBold, Enum.AutomaticSize.Y local status = label("Status", "Opening...", body, 13) status.LayoutOrder, status.TextColor3, status.AutomaticSize = 2, colors.muted, Enum.AutomaticSize.Y local progress = create("Frame", "Progress", body, { Size = UDim2.new(1, 0, 0, 3), BackgroundColor3 = colors.surface, BorderSizePixel = 0, LayoutOrder = 3, ClipsDescendants = true, }) round(progress, 2) local progressFill = create("Frame", "Fill", progress, { Size = UDim2.fromScale(0.28, 1), BackgroundColor3 = colors.blue, BorderSizePixel = 0, Visible = false, }) round(progressFill, 2) local licenseGroup = create("Frame", "License", body, { Size = UDim2.new(1, 0, 0, 72), BackgroundTransparency = 1, LayoutOrder = 4 }) local licenseLabel = label("Label", "License key", licenseGroup, 12) licenseLabel.Size, licenseLabel.TextColor3 = UDim2.new(1, 0, 0, 18), colors.muted local license = create("TextBox", "LicenseKey", licenseGroup, { Position = UDim2.fromOffset(0, 28), Size = UDim2.new(1, 0, 0, 44), BackgroundColor3 = colors.surface, BorderSizePixel = 0, Font = Enum.Font.Gotham, TextSize = 13, TextColor3 = colors.text, PlaceholderColor3 = colors.muted, PlaceholderText = "Enter your license key", Text = "", ClearTextOnFocus = false, TextWrapped = false, MultiLine = false, TextXAlignment = Enum.TextXAlignment.Left, }) round(license) local licenseOutline = outline(license) create("UIPadding", "Padding", license, { PaddingLeft = UDim.new(0, 12), PaddingRight = UDim.new(0, 12) }) local function button(name, text, order, primary) local item = create("TextButton", name, body, { Size = UDim2.new(1, 0, 0, 42), BackgroundColor3 = primary and colors.blue or colors.surface, BorderSizePixel = 0, Font = Enum.Font.GothamMedium, TextSize = 13, TextColor3 = colors.text, Text = text, TextWrapped = true, AutoButtonColor = false, LayoutOrder = order, }) round(item) if not primary then outline(item) end local function hover(on) if not item.Active then return end animate(item, { BackgroundColor3 = primary and (on and colors.blueHover or colors.blue) or (on and colors.hover or colors.surface) }) end item.MouseEnter:Connect(function() hover(true) end) item.MouseLeave:Connect(function() hover(false) end) item.SelectionGained:Connect(function() hover(true) end) item.SelectionLost:Connect(function() hover(false) end) item.MouseButton1Down:Connect(function() if item.Active then animate(item, { BackgroundColor3 = colors.border }, 0.08) end end) item.MouseButton1Up:Connect(function() hover(false) end) return item end local installButton = button("Install", "Install", 5, true) local removeButton = button("Remove", "Uninstall", 6, false) local cancelButton = button("Cancel", "Cancel", 7, false) local function stopMotion() if progressTween then progressTween:Cancel(); progressTween = nil end for item, tween in pairs(activeTweens) do tween:Cancel(); activeTweens[item] = nil end body.GroupTransparency = 0 end local function showProgress() if progressTween then progressTween:Cancel(); progressTween = nil end progress.Visible = busy progressFill.Visible = busy progressFill.Position = UDim2.fromScale(0, 0) if busy and widget.Enabled and not unloaded and not reducedMotion() then progressTween = Tweens:Create(progressFill, TweenInfo.new(0.8, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, -1, true), { Position = UDim2.fromScale(0.72, 0) }) progressTween:Play() end end local function setStatus(title, message, tone) heading.Text, status.Text = title, message status.TextTransparency = 0.3 status.TextColor3 = tone == "error" and colors.red or tone == "success" and colors.green or colors.muted animate(status, { TextTransparency = 0 }, 0.2) end local function render(transition) licenseGroup.Visible = credential == nil and not confirmingRemoval license.TextEditable = contextReady and not busy installButton.Visible = not confirmingRemoval installButton.Text = busy and operation == "install" and (hasManaged and "Updating..." or "Installing...") or (hasManaged and "Update" or "Install") removeButton.Visible = hasManaged and (credential ~= nil or runtimeToken ~= nil) removeButton.Text = busy and operation == "remove" and "Removing..." or confirmingRemoval and "Uninstall RoSafe" or "Uninstall" removeButton.TextColor3 = confirmingRemoval and colors.red or colors.muted cancelButton.Visible = confirmingRemoval for _, item in ipairs({ installButton, removeButton, cancelButton }) do local enabled = contextReady and not busy and not unloaded and (item ~= installButton or not updateRequired) item.Active, item.Interactable, item.Selectable = enabled, enabled, enabled animate(item, { BackgroundTransparency = enabled and 0 or 0.5, TextTransparency = enabled and 0 or 0.45 }) end if transition then body.GroupTransparency = 0.35; animate(body, { GroupTransparency = 0 }, 0.22) end showProgress() end local messages = { publish = "Publish this game to Roblox first.", login = "Sign into Roblox Studio first.", playing = "Stop the play test first.", context = "The Studio account or game changed. Reopen RoSafe.", license = "Enter a valid license key.", invalid = "Invalid or revoked license.", universe = "This license belongs to another game.", binding = "Link this game in your RoSafe dashboard first.", update = "Update the plugin in Studio, then reopen it.", rate = "Too many requests. Try again shortly.", network = "Couldn't reach RoSafe. Try again.", bundle = "The install package is unavailable. Try again shortly.", conflict = "Another object is named RoSafe. Rename it and retry.", editing = "Another Studio edit is active. Try again.", changed = "A script changed during installation. Try again.", storage = "Couldn't save the installation. Try again.", rollback = "Couldn't restore every change. Check the RoSafe scripts before retrying.", install = "Installation failed. Check Studio permissions and retry.", remove = "Removal failed. Check Studio permissions and retry.", opening = "Couldn't read the installation. Reopen RoSafe.", } local function ensure(condition, code) if not condition then error({ code = code }, 0) end end local function owned(item, role) return item and Tags:HasTag(item, TAG) and item:GetAttribute("RoSafeOwner") == "RoSafe" and (not role or item:GetAttribute("RoSafeRole") == role) end local function tag(item, role) Tags:AddTag(item, TAG) item:SetAttribute("RoSafeOwner", "RoSafe") item:SetAttribute("RoSafeRole", role) item:SetAttribute("RoSafeVersion", VERSION) end local function at(parent, name, class, role) local result for _, item in ipairs(parent:GetChildren()) do if item.Name == name then ensure(not result and item:IsA(class) and owned(item, role), "conflict") result = item end end return result end local function contextValid() return not unloaded and not Run:IsRunning() and game.GameId == activeUniverse and game.PlaceId == activePlace and Studio:GetUserId() == boundUser and activeUniverse and activeUniverse > 0 and boundUser and boundUser > 0 end local function requireContext() ensure(not Run:IsRunning(), "playing") ensure(contextValid(), "context") end local function request(path, payload) requireContext() local ok, response = pcall(function() return Http:RequestAsync({ Url = API .. path, Method = payload and "POST" or "GET", Headers = { ["Content-Type"] = "application/json" }, Body = payload and Http:JSONEncode(payload) or nil }) end) requireContext() ensure(ok, "network") if not response.Success then local codes = { [401] = "invalid", [403] = "universe", [409] = "binding", [422] = "license", [426] = "update", [429] = "rate" } error({ code = path == "/activate" and codes[response.StatusCode] or (response.StatusCode == 429 and "rate") or "network" }, 0) end local decoded, data = pcall(function() return Http:JSONDecode(response.Body) end) ensure(decoded and type(data) == "table", "bundle") return data end local function atLeast(actual, minimum) if type(actual) ~= "string" or type(minimum) ~= "string" then return false end local a, b, c = actual:match("^(%d+)%.(%d+)%.(%d+)$") local x, y, z = minimum:match("^(%d+)%.(%d+)%.(%d+)$") if not a or not x then return false end local left, right = { tonumber(a), tonumber(b), tonumber(c) }, { tonumber(x), tonumber(y), tonumber(z) } for index = 1, 3 do if left[index] ~= right[index] then return left[index] > right[index] end end return true end local function reportHttp() if unloaded or not runtimeToken or not installed or not contextValid() then return end local enabled, token = Http.HttpEnabled == true, runtimeToken if reportedHttp == enabled then return end local ok, response = pcall(function() return Http:RequestAsync({ Url = API .. "/plugin/status", Method = "POST", Headers = { ["Content-Type"] = "application/json", Authorization = "Bearer " .. token }, Body = Http:JSONEncode({ http_enabled = enabled }) }) end) if ok and response.Success and runtimeToken == token then reportedHttp = enabled end end local function getRelease() local release = request("/release") ensure(atLeast(release.version, "0.0.0") and atLeast(release.min_agent, "0.0.0") and atLeast(release.min_plugin, "0.0.0"), "bundle") updateRequired = not atLeast(VERSION, release.min_plugin) ensure(not updateRequired, "update") return release end local function managed() local serverService, storage = game:GetService("ServerScriptService"), game:GetService("ServerStorage") local starter = game:GetService("StarterPlayer"):WaitForChild("StarterPlayerScripts") requireContext() local folder = at(serverService, "RoSafe", "Folder", "server-folder") return { folder = folder, server = folder and at(folder, "RoSafe", "Script", "server"), guard = at(storage, "RoSafe", "ModuleScript", "guard"), client = at(starter, "RoSafe", "LocalScript", "client"), serverService = serverService, storage = storage, starter = starter, } end local function inspectInstallation() local items = managed() hasManaged = items.server ~= nil or items.guard ~= nil or items.client ~= nil installed = items.server ~= nil and items.guard ~= nil and items.client ~= nil and items.server.Enabled and items.client.Enabled installedVersion = items.server and items.server:GetAttribute("RoSafeVersion") or nil if installed then installed = type(installedVersion) == "string" and items.guard:GetAttribute("RoSafeVersion") == installedVersion and items.client:GetAttribute("RoSafeVersion") == installedVersion end return items end local function sameSetting(left, right) if type(left) ~= "table" or type(right) ~= "table" then return left == right end for key, value in pairs(left) do if right[key] ~= value then return false end end for key, value in pairs(right) do if left[key] ~= value then return false end end return true end local function writeSetting(value) local ok = pcall(function() plugin:SetSetting(settingsKey, value) ensure(sameSetting(plugin:GetSetting(settingsKey), value), "storage") end) ensure(ok, "storage") end local function source(item) return Editor:GetEditorSource(item) end local function replaceSource(item, before, after, rollback) Editor:UpdateSourceAsync(item, function(current) if not rollback then requireContext() end ensure(current == before, "changed") return after end) end local function removeOwned(item) if not owned(item) then return end for _, child in ipairs(item:GetChildren()) do removeOwned(child) end if #item:GetChildren() == 0 then item:Destroy() end end local function restoreSnapshot(snapshot) local restored = true for index = #snapshot.edited, 1, -1 do local entry = snapshot.edited[index] local ok = pcall(function() ensure(owned(entry.item) and entry.item.Parent == entry.parent, "rollback") if source(entry.item) == entry.after then replaceSource(entry.item, entry.after, entry.before, true) end ensure(source(entry.item) == entry.before, "rollback") if entry.enabled ~= nil then entry.item.Enabled = entry.enabled end entry.item:SetAttribute("RoSafeVersion", entry.version) end) restored = ok and restored end for index = #snapshot.created, 1, -1 do local ok = pcall(function() removeOwned(snapshot.created[index]) ensure(snapshot.created[index].Parent == nil, "rollback") end) restored = ok and restored end for index = #snapshot.detached, 1, -1 do local entry = snapshot.detached[index] local ok = pcall(function() ensure(owned(entry.item), "rollback") if entry.item.Parent == nil then ensure(not entry.parent:FindFirstChild(entry.item.Name), "rollback") entry.item.Parent = entry.parent end ensure(entry.item.Parent == entry.parent, "rollback") if entry.enabled ~= nil then entry.item.Enabled = entry.enabled end end) restored = ok and restored end return restored end local function transaction(name, edit, nextSetting) requireContext() local previousSetting, targetKey = plugin:GetSetting(settingsKey), settingsKey local recording = History:TryBeginRecording("RoSafe" .. name, name .. " RoSafe") ensure(recording, "editing") local snapshot = { edited = {}, created = {}, detached = {} } local settingAttempted = false local ok, err = pcall(function() edit(snapshot) requireContext() settingAttempted = true writeSetting(nextSetting()) requireContext() History:FinishRecording(recording, Enum.FinishRecordingOperation.Commit) end) if not ok then local restored = restoreSnapshot(snapshot) if settingAttempted then local saved = pcall(function() plugin:SetSetting(targetKey, previousSetting) ensure(sameSetting(plugin:GetSetting(targetKey), previousSetting), "storage") end) restored = saved and restored end local cancelled = pcall(function() History:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel) end) ensure(restored and cancelled, "rollback") error(err, 0) end return snapshot end local function install(scripts, releaseVersion, newSetting) requireContext() ensure(type(scripts) == "table", "bundle") for _, key in ipairs({ "server", "guard", "client" }) do ensure(type(scripts[key]) == "string" and #scripts[key] > 0 and #scripts[key] < 250000, "bundle") ensure(not scripts[key]:find("__RUNTIME_TOKEN__", 1, true) and not scripts[key]:find("__UNIVERSE_ID__", 1, true), "bundle") end local items = managed() transaction("Install", function(snapshot) requireContext() local folder = items.folder if not folder then folder = Instance.new("Folder") folder.Name = "RoSafe" tag(folder, "server-folder") table.insert(snapshot.created, folder) folder.Parent = items.serverService end local function put(item, class, parent, role, code) requireContext() if item then local before = source(item) local entry = { item = item, parent = parent, before = before, after = code, version = item:GetAttribute("RoSafeVersion") } if item:IsA("BaseScript") then entry.enabled = item.Enabled end table.insert(snapshot.edited, entry) replaceSource(item, before, code) else item = Instance.new(class) item.Name = "RoSafe" tag(item, role) table.insert(snapshot.created, item) item.Source = code item.Parent = parent end requireContext() item:SetAttribute("RoSafeVersion", releaseVersion) if item:IsA("BaseScript") then item.Enabled = true end end put(items.guard, "ModuleScript", items.storage, "guard", scripts.guard) put(items.server, "Script", folder, "server", scripts.server) put(items.client, "LocalScript", items.starter, "client", scripts.client) end, function() return newSetting end) end local function activate() requireContext() local key = credential or license.Text:match("^%s*(.-)%s*$") ensure(type(key) == "string" and #key > 0 and #key <= 150 and not key:find("[%s%c]"), "license") setStatus(hasManaged and "Updating RoSafe" or "Installing RoSafe", "Checking license...") local release = getRelease() local httpEnabled = Http.HttpEnabled == true local data = request("/activate", { license_key = key, universe_id = activeUniverse, plugin_version = VERSION, roblox_user_id = boundUser, http_enabled = httpEnabled }) ensure(type(data.runtime_token) == "string" and #data.runtime_token > 0 and #data.runtime_token <= 512 and not data.runtime_token:find("[%s%c]") and type(data.game_id) == "number" and data.game_id > 0 and data.game_id % 1 == 0 and atLeast(data.version, release.min_agent), "bundle") local nextSaved = { license = key, runtime_token = data.runtime_token, version = data.version, universe_id = activeUniverse, user_id = boundUser, client = true } setStatus(hasManaged and "Updating RoSafe" or "Installing RoSafe", "Adding scripts...") install(data.scripts, data.version, nextSaved) credential, runtimeToken, savedState = key, data.runtime_token, nextSaved reportedHttp = httpEnabled license.Text = "" inspectInstallation() ensure(installed, "install") setStatus("Installed", Http.HttpEnabled and "Publish your game to apply." or HTTP_OFF, "success") end local function uninstall() requireContext() ensure(hasManaged and (credential ~= nil or runtimeToken ~= nil), "license") if not confirmingRemoval then confirmingRemoval = true setStatus("Uninstall RoSafe?", "Remove RoSafe from this place?") return end local items, retained = managed(), false local snapshot = transaction("Uninstall", function(state) local function detach(item) if not owned(item) then return end requireContext() for _, child in ipairs(item:GetChildren()) do detach(child) end local entry = { item = item, parent = item.Parent } if item:IsA("BaseScript") then entry.enabled = item.Enabled; item.Enabled = false end table.insert(state.detached, entry) if #item:GetChildren() == 0 then item.Parent = nil elseif item:IsA("LuaSourceContainer") then retained = true end end for _, item in pairs({ items.folder, items.guard, items.client }) do detach(item) end end, function() return retained and savedState or nil end) for _, entry in ipairs(snapshot.detached) do if entry.item.Parent == nil and #entry.item:GetChildren() == 0 then pcall(function() entry.item:Destroy() end) end end confirmingRemoval = false inspectInstallation() if retained then setStatus("Removal incomplete", "Move your objects out of RoSafe, then retry.", "error") else credential, runtimeToken, savedState = nil, nil, nil license.Text = "" setStatus("Removed", "Publish your game to apply.", "success") end end local opening local function work(kind, callback) if busy or unloaded then return end busy, operation = true, kind render(false) local ok, err = pcall(callback) busy, operation = false, nil if not unloaded then if not ok then local code = type(err) == "table" and err.code or kind if code == "context" or code == "playing" then contextReady = false end if code == "update" then updateRequired = true end setStatus(kind == "install" and "Couldn't install" or kind == "remove" and "Couldn't remove" or "Needs attention", messages[code] or messages[kind], "error") end render(true) if refreshPending then refreshPending = false if widget.Enabled then task.defer(opening) end end end end local function idleStatus() if installed then setStatus("Installed", Http.HttpEnabled and "Installed in this place." or HTTP_OFF, "success") elseif hasManaged then setStatus("Installation incomplete", "Update RoSafe to finish installing.") else setStatus("Install RoSafe", credential and "Ready to install." or "Enter your license key.") end end opening = function() if busy then refreshPending = true; return end work("opening", function() activeUniverse, activePlace, boundUser = game.GameId, game.PlaceId, Studio:GetUserId() credential, runtimeToken, savedState, installedVersion, reportedHttp = nil, nil, nil, nil, nil installed, hasManaged, confirmingRemoval, contextReady, updateRequired = false, false, false, false, false license.Text = "" settingsKey = "RoSafeV1:" .. tostring(boundUser) .. ":" .. tostring(activeUniverse) gameName.Text = type(game.Name) == "string" and game.Name ~= "" and game.Name or "Current experience" thumbnail.Image = activeUniverse > 0 and ("rbxthumb://type=GameIcon&id=" .. tostring(activeUniverse) .. "&w=150&h=150") or "" ensure(activeUniverse > 0 and activePlace > 0, "publish") ensure(boundUser > 0, "login") requireContext() contextReady = true savedState = plugin:GetSetting(settingsKey) if type(savedState) == "table" and (savedState.universe_id == nil or savedState.universe_id == activeUniverse) and (savedState.user_id == nil or savedState.user_id == boundUser) then if type(savedState.license) == "string" and #savedState.license > 0 and #savedState.license <= 150 then credential = savedState.license end if type(savedState.runtime_token) == "string" and #savedState.runtime_token > 0 and #savedState.runtime_token <= 512 then runtimeToken = savedState.runtime_token end end inspectInstallation() setStatus(installed and "Installed" or "Install RoSafe", "Checking for updates...") render(false) local release = getRelease() idleStatus() if installed and not atLeast(installedVersion, release.version) then setStatus("Update available", "Update RoSafe, then publish your game.") end task.spawn(reportHttp) end) end open.Click:Connect(function() widget.Enabled = not widget.Enabled end) widget:GetPropertyChangedSignal("Enabled"):Connect(function() open:SetActive(widget.Enabled) if widget.Enabled then opening() else stopMotion() end end) installButton.Activated:Connect(function() if contextReady and not updateRequired then work("install", activate) end end) license.FocusLost:Connect(function(enterPressed) if enterPressed and contextReady and not updateRequired then work("install", activate) end end) license.Focused:Connect(function() animate(licenseOutline, { Color = colors.blue }) end) license.FocusLost:Connect(function() animate(licenseOutline, { Color = colors.border }) end) removeButton.Activated:Connect(function() if contextReady then work("remove", uninstall) end end) pcall(function() Http:GetPropertyChangedSignal("HttpEnabled"):Connect(function() if unloaded then return end if installed and contextReady and not busy and not confirmingRemoval then idleStatus() end task.spawn(reportHttp) end) end) cancelButton.Activated:Connect(function() if busy or unloaded then return end confirmingRemoval = false idleStatus() render(true) end) plugin.Unloading:Connect(function() unloaded = true; license.Text = ""; stopMotion() end) render(false) if widget.Enabled then task.defer(opening) end