RobloxBlocks false 0 {DC61827E-2576-5CB1-9DA4-56DBF957FA1F} 8 do table.remove(logLines) end logLabel.Text = table.concat(logLines, "\n") end local function setStatus(kind, text) local colors = { off = Color3.fromRGB(150, 150, 150), waiting = Color3.fromRGB(255, 191, 0), online = Color3.fromRGB(15, 189, 140), error = Color3.fromRGB(255, 102, 128), } statusDot.BackgroundColor3 = colors[kind] or colors.off statusText.Text = text end local function refreshMode() local robx = mode == "robx" robxSection.Visible = robx localSection.Visible = not robx robxModeButton.BackgroundColor3 = robx and Color3.fromRGB(0, 102, 204) or Color3.fromRGB(70, 70, 70) localModeButton.BackgroundColor3 = robx and Color3.fromRGB(70, 70, 70) or Color3.fromRGB(0, 102, 204) robxModeButton.TextColor3 = Color3.new(1, 1, 1) localModeButton.TextColor3 = Color3.new(1, 1, 1) local paired = robxKey ~= nil accountLabel.Visible = paired accountLabel.Text = "Аккаунт RobX: " .. (robxName or "подключён") codeLabel.Visible = not paired codeBox.Visible = not paired forgetButton.Visible = paired if paired then robxHint.Text = "Собирай блоки в RobX Code: robx.online/code → вкладка «Код» → Roblox." else robxHint.Text = "1. Открой robx.online/code → вкладка «Код» → Roblox.\n" .. "2. Нажми «Подключить Studio» — там будет код.\n" .. "3. Введи код сюда и нажми «Подключить»." end end local function refreshButton() if connected then connectButton.Text = "Отключить" connectButton.BackgroundColor3 = Color3.fromRGB(255, 102, 128) else connectButton.Text = "Подключить" connectButton.BackgroundColor3 = Color3.fromRGB(15, 189, 140) end toolbarButton:SetActive(widget.Enabled) end --------------------------------------------------------------------------- -- HTTP --------------------------------------------------------------------------- -- Запрос: ok, ответ (таблица) или текст ошибки, код ответа local function request(method, url, body, headers) local options = { Url = url, Method = method, Headers = headers or {}, } if body ~= nil then options.Headers["Content-Type"] = "application/json" options.Body = HttpService:JSONEncode(body) end local ok, response = pcall(function() return HttpService:RequestAsync(options) end) if not ok then return false, tostring(response), 0 end local decodedOk, decoded = pcall(function() return HttpService:JSONDecode(response.Body) end) if not decodedOk or type(decoded) ~= "table" then decoded = nil end if not response.Success then if decoded then return false, decoded, response.StatusCode end return false, tostring(response.StatusCode) .. " " .. tostring(response.StatusMessage), response.StatusCode end if not decoded then return false, "сервер ответил не JSON", response.StatusCode end return true, decoded, response.StatusCode end -- Запрос к «почтовому ящику» плагина: свой сервер или ретранслятор RobX local function studioRequest(method, path, body) if mode == "robx" then return request(method, ROBX_RELAY_URL .. "/studio" .. path, body, { ["X-Studio-Key"] = robxKey or "" }) end return request(method, serverUrl .. "/api/studio" .. path, body) end local function problemText(problem) if type(problem) == "table" then return tostring(problem.error or problem.message or "ошибка") end return tostring(problem) end --------------------------------------------------------------------------- -- Объекты и скрипты --------------------------------------------------------------------------- local function isBlocksScript(instance) return instance:IsA("Script") and instance:GetAttribute(PROGRAM_ATTRIBUTE) ~= nil end local function findBlocksScript(target) for _, child in ipairs(target:GetChildren()) do if isBlocksScript(child) then return child end end return nil end -- Какие объекты можно программировать: детали, модели и «вся игра» local function resolveTarget(instance) if not instance then return nil end if isBlocksScript(instance) then instance = instance.Parent if not instance then return nil end end if instance == ServerScriptService then return instance end if instance:IsA("Workspace") or instance:IsA("Terrain") or instance:IsA("Camera") then return nil end if instance:IsA("BasePart") or instance:IsA("Model") then return instance end return nil end local function idFor(instance) if instance == ServerScriptService then return GAME_ID end local id = idOfInstance[instance] if not id then id = HttpService:GenerateGUID(false) idOfInstance[instance] = id instanceOfId[id] = instance end return id end local function instanceFor(id) if id == GAME_ID then return ServerScriptService end local instance = instanceOfId[id] if instance and instance:IsDescendantOf(game) then return instance end return nil end local function describe(target) local blocksScript = findBlocksScript(target) local program = blocksScript and blocksScript:GetAttribute(PROGRAM_ATTRIBUTE) or nil if program == "" then program = nil end local isGame = target == ServerScriptService return { id = idFor(target), name = isGame and "Вся игра" or target.Name, className = target.ClassName, path = target:GetFullName(), kind = isGame and "game" or "object", program = program, } end local function showTarget() if currentTarget then if currentTarget == ServerScriptService then targetLabel.Text = "Объект: 🌍 вся игра (ServerScriptService)" else targetLabel.Text = "Объект: " .. currentTarget.Name .. " (" .. currentTarget:GetFullName() .. ")" end else targetLabel.Text = "Объект: выдели деталь или модель в мире" end end local function sendSelection(force) if not online or not currentTarget then return end if not force and lastSentTarget == currentTarget then return end lastSentTarget = currentTarget local target = currentTarget task.spawn(function() local ok, problem = studioRequest("POST", "/selection", { target = describe(target) }) if not ok then log("Не удалось отправить выделение: " .. problemText(problem)) end end) end local function onSelectionChanged() local target = resolveTarget(Selection:Get()[1]) if target and target ~= currentTarget then currentTarget = target showTarget() sendSelection(false) end end --------------------------------------------------------------------------- -- Запись программы в игру --------------------------------------------------------------------------- local function writeSource(blocksScript, source) local ok = pcall(function() ScriptEditorService:UpdateSourceAsync(blocksScript, function() return source end) end) if not ok then blocksScript.Source = source end end -- Нажатия клавиш передаёт на сервер маленький LocalScript local function ensureKeyListener() if not ReplicatedStorage:FindFirstChild(KEY_EVENT_NAME) then local remote = Instance.new("RemoteEvent") remote.Name = KEY_EVENT_NAME remote.Parent = ReplicatedStorage end local playerScripts = StarterPlayer:FindFirstChildOfClass("StarterPlayerScripts") if not playerScripts then error("не найдена папка StarterPlayer.StarterPlayerScripts", 0) end if not playerScripts:FindFirstChild(KEY_LISTENER_NAME) then local listener = Instance.new("LocalScript") listener.Name = KEY_LISTENER_NAME listener.Parent = playerScripts writeSource(listener, KEY_LISTENER_SOURCE) log("Добавлен скрипт для клавиш: " .. listener:GetFullName()) end end local function saveProgram(target, update) local program = type(update.program) == "string" and update.program or "" local code = type(update.code) == "string" and update.code or "" local blocksScript = findBlocksScript(target) if not blocksScript and update.empty then return false -- пустую программу не сохраняем, чтобы не плодить скрипты end local recording = ChangeHistoryService:TryBeginRecording("RobloxBlocksSave", "Блоки: программа " .. target.Name) local ok, problem = pcall(function() if not blocksScript then blocksScript = Instance.new("Script") blocksScript.Name = SCRIPT_NAME blocksScript:SetAttribute(PROGRAM_ATTRIBUTE, "") blocksScript.Parent = target end blocksScript:SetAttribute(PROGRAM_ATTRIBUTE, program) writeSource(blocksScript, code) if update.usesKeys then ensureKeyListener() end end) if recording then local operation = ok and Enum.FinishRecordingOperation.Commit or Enum.FinishRecordingOperation.Cancel ChangeHistoryService:FinishRecording(recording, operation) end if not ok then error(problem, 0) end return true end local function report(update, ok, message) task.spawn(function() studioRequest("POST", "/result", { id = update.id, ok = ok, message = message, version = update.version, }) end) end local function handleUpdate(update) if type(update) ~= "table" then return end if update.type == "save" then local target = instanceFor(update.id) if not target then report(update, false, "объект не найден — выдели его в Studio ещё раз") log("Объект для сохранения не найден") return end local ok, result = pcall(saveProgram, target, update) if ok then report(update, true, "Сохранено") if result then log("Сохранено: " .. target.Name) end else report(update, false, tostring(result)) log("Ошибка сохранения: " .. tostring(result)) end elseif update.type == "command" then if update.command == "selectGame" then Selection:Set({ ServerScriptService }) currentTarget = ServerScriptService showTarget() sendSelection(false) elseif update.command == "select" then local target = instanceFor(update.id) if target and target ~= ServerScriptService then Selection:Set({ target }) end end end end --------------------------------------------------------------------------- -- Подключение --------------------------------------------------------------------------- local forgetKey -- объявлена ниже local function runSession(mySession) local since = 0 local greeted = false local failures = 0 while session == mySession do if not greeted then setStatus("waiting", mode == "robx" and "Подключаюсь к RobX…" or "Ищу редактор…") local ok, result, status = studioRequest("POST", "/hello", { place = game.Name, protocol = PROTOCOL }) if session ~= mySession then break end if ok and result.name == "roblox-blocks" then greeted = true online = true failures = 0 if mode == "robx" then if type(result.user) == "string" and result.user ~= "" then robxName = result.user plugin:SetSetting("RobxName", robxName) refreshMode() end setStatus("online", "Подключено к RobX Code") log("Подключено к RobX" .. (robxName and (": " .. robxName) or "")) else setStatus("online", "Подключено к редактору") log("Подключено: " .. serverUrl) end sendSelection(true) elseif mode == "robx" and status == 401 then forgetKey("Подключение отозвано. Возьми новый код в RobX Code.") break else online = false failures = failures + 1 if mode == "robx" then setStatus("waiting", "Нет связи с RobX. Проверь интернет.") else setStatus("waiting", "Редактор не отвечает. Запущен ли npm start?") end if failures == 1 then log("Нет ответа: " .. problemText(result)) end task.wait(math.min(1 + failures, 8)) end else local ok, result, status = studioRequest("GET", "/updates?since=" .. tostring(since)) if session ~= mySession then break end if ok then since = tonumber(result.version) or since for _, update in ipairs(result.updates or {}) do handleUpdate(update) end elseif mode == "robx" and status == 401 then forgetKey("Подключение отозвано. Возьми новый код в RobX Code.") break else greeted = false online = false setStatus("waiting", "Связь потеряна, переподключаюсь…") log("Связь потеряна: " .. problemText(result)) task.wait(2) end end end end local function startSession() session = session + 1 connected = true online = false lastSentTarget = nil plugin:SetSetting("AutoConnect", true) refreshButton() local mySession = session task.spawn(runSession, mySession) end -- Почему код не подошёл — понятным языком local CLAIM_ERRORS = { invalid_code = "Код не подошёл. Проверь буквы и цифры.", code_expired = "Срок кода вышел. Возьми новый в RobX Code.", code_replaced = "Этот код заменён новым. Введи последний код из RobX Code.", code_used = "Этот код уже использован. Возьми новый в RobX Code.", too_many_attempts = "Слишком много попыток. Подожди 10 минут.", } -- Обменять код из RobX Code на ключ Studio и подключиться local function claimAndConnect(code) local mySession = session + 1 session = mySession connected = true refreshButton() setStatus("waiting", "Проверяю код…") task.spawn(function() local ok, result = request("POST", ROBX_API_URL .. "/game/roblox/claim", { code = code, label = "Studio · " .. game.Name, }) if session ~= mySession then return end if ok and type(result.key) == "string" and result.key ~= "" then robxKey = result.key robxName = type(result.name) == "string" and result.name ~= "" and result.name or nil plugin:SetSetting("RobxKey", robxKey) plugin:SetSetting("RobxName", robxName) codeBox.Text = "" refreshMode() log("Studio привязана к RobX" .. (robxName and (": " .. robxName) or "")) startSession() else connected = false refreshButton() local code = type(result) == "table" and result.error or nil local text = CLAIM_ERRORS[code] or ("Нет связи с RobX: " .. problemText(result)) setStatus("error", text) log(text) end end) end local function connect() if mode == "robx" then if robxKey then startSession() return end -- Код вводят как видят: строчными, с пробелом — API это понимает local code = codeBox.Text:gsub("%s+", "") if code == "" then setStatus("error", "Введи код из RobX Code") return end claimAndConnect(code) return end local url = urlBox.Text:gsub("%s+", ""):gsub("/+$", "") if url == "" then url = DEFAULT_URL end if not url:match("^https?://") then url = "http://" .. url end serverUrl = url urlBox.Text = url plugin:SetSetting("ServerUrl", url) startSession() end local function disconnect(remember) session = session + 1 if online then task.spawn(studioRequest, "POST", "/bye", {}) end connected = false online = false if remember then plugin:SetSetting("AutoConnect", false) end setStatus("off", "Не подключено") refreshButton() end forgetKey = function(reason) disconnect(false) robxKey = nil robxName = nil plugin:SetSetting("RobxKey", nil) plugin:SetSetting("RobxName", nil) refreshMode() if reason then setStatus("error", reason) log(reason) end end local function setMode(newMode) if mode == newMode then return end if connected then disconnect(false) end mode = newMode plugin:SetSetting("Mode", mode) refreshMode() end robxModeButton.Activated:Connect(function() setMode("robx") end) localModeButton.Activated:Connect(function() setMode("local") end) forgetButton.Activated:Connect(function() forgetKey(nil) log("Вышли из RobX на этом компьютере") end) -- Можно ли подключаться без вопросов: в режиме RobX нужен ключ local function canAutoConnect() return mode == "local" or robxKey ~= nil end connectButton.Activated:Connect(function() if connected then disconnect(true) log("Отключено") else connect() end end) toolbarButton.Click:Connect(function() widget.Enabled = not widget.Enabled if widget.Enabled and not connected and canAutoConnect() then connect() end refreshButton() end) widget:GetPropertyChangedSignal("Enabled"):Connect(refreshButton) Selection.SelectionChanged:Connect(onSelectionChanged) -- Ctrl+Z в Studio может вернуть старую программу — сообщаем редактору local function onHistoryChanged() if currentTarget then sendSelection(true) end end ChangeHistoryService.OnUndo:Connect(onHistoryChanged) ChangeHistoryService.OnRedo:Connect(onHistoryChanged) plugin.Unloading:Connect(function() disconnect(false) end) onSelectionChanged() refreshMode() refreshButton() if plugin:GetSetting("AutoConnect") == true and canAutoConnect() then connect() end ]]>