Загрузка данных


local Players = game:GetService("Players")
local HttpService = game:GetService("HttpService")
local TeleportService = game:GetService("TeleportService")
local GuiService = game:GetService("GuiService")
local CoreGui = game:GetService("CoreGui")

local MAX_PLAYERS = 3
local placeId = game.PlaceId
local currentJobId = game.JobId

local env = (getgenv and getgenv()) or _G
env.RunId = (env.RunId or 0) + 1
local myRunId = env.RunId
env.AttemptId = 0
env.Failed = env.Failed or {}
env.Pool = env.Pool or {}
env.Busy = false
env.IsHopping = false

if env.FailConn then env.FailConn:Disconnect() end
if env.JoinConn then env.JoinConn:Disconnect() end

local cacheFile = "HopCache_" .. tostring(placeId) .. ".json"

local function loadCache()
    if isfile and readfile and isfile(cacheFile) then
        local ok, res = pcall(function() return HttpService:JSONDecode(readfile(cacheFile)) end)
        if ok and type(res) == "table" then
            for k, v in pairs(res.failed or {}) do env.Failed[k] = v end
            if res.time and (os.time() - res.time) < 120 and type(res.pool) == "table" then
                return res.pool
            end
        end
    end
    return nil
end

local function saveCache()
    if writefile then
        pcall(function()
            writefile(cacheFile, HttpService:JSONEncode({ time = os.time(), pool = env.Pool, failed = env.Failed }))
        end)
    end
end

local function clearErrorPopup()
    pcall(function()
        GuiService:ClearError()
        local overlay = CoreGui:FindFirstChild("RobloxPromptGui") and CoreGui.RobloxPromptGui:FindFirstChild("promptOverlay")
        if overlay then
            local prompt = overlay:FindFirstChild("ErrorPrompt")
            if prompt and prompt.Visible then
                local btn = prompt:FindFirstChildWhichIsA("TextButton", true) or prompt:FindFirstChildWhichIsA("ImageButton", true)
                if btn and firesignal then firesignal(btn.MouseButton1Click) end
                prompt.Visible = false
            end
        end
        for _, v in ipairs(CoreGui:GetDescendants()) do
            if v:IsA("TextLabel") and v.Visible then
                local txt = string.lower(v.Text or "")
                if string.find(txt, "unknown status") or string.find(txt, "unavailable") or string.find(txt, "ended") then
                    local frame = v:FindFirstAncestorWhichIsA("Frame")
                    if frame then frame.Visible = false end
                end
            end
        end
    end)
end

local function hasError()
    local ok1, code = pcall(function() return GuiService:GetErrorCode() end)
    if ok1 and code and code ~= Enum.ConnectionError.OK then return true end

    local ok2, vis = pcall(function()
        local promptGui = CoreGui:FindFirstChild("RobloxPromptGui")
        local overlay = promptGui and promptGui:FindFirstChild("promptOverlay")
        local prompt = overlay and overlay:FindFirstChild("ErrorPrompt")
        return (promptGui and promptGui.Enabled ~= false) and (prompt and prompt.Visible == true) or false
    end)
    return ok2 and vis
end

local scanServers

local function hopNext()
    if env.RunId ~= myRunId then return end
    if env.IsHopping then return end
    env.IsHopping = true

    if #env.Pool == 0 then env.Pool = loadCache() or {} end

    while #env.Pool > 0 do
        local id = table.remove(env.Pool, 1)
        if not env.Failed[id] and id ~= currentJobId then
            env.Failed[id] = true
            env.Busy = true
            env.AttemptId = env.AttemptId + 1
            local thisAttempt = env.AttemptId

            saveCache()
            clearErrorPopup()
            print("[AutoHop] Подключение: " .. id .. " (В пуле осталось: " .. #env.Pool .. ")")

            local ok, err = pcall(function()
                TeleportService:TeleportToPlaceInstance(placeId, id, Players.LocalPlayer)
            end)

            if ok then
                -- Сторожевой таймер: ждем 7 секунд до следующего шага
                task.delay(7, function()
                    if env.RunId == myRunId and env.AttemptId == thisAttempt and game.JobId == currentJobId then
                        warn("[AutoHop] Сервер не ответил (Timeout). Берем следующий...")
                        clearErrorPopup()
                        env.Busy = false
                        env.IsHopping = false
                        hopNext()
                    end
                end)
                env.IsHopping = false
                return
            else
                warn("[AutoHop] Ошибка вызова (кулдаун движка): " .. tostring(err) .. ". Ждем 3.5 сек...")
                task.wait(3.5)
            end
        end
    end

    env.Busy = false
    env.IsHopping = false

    if hasError() then
        clearErrorPopup()
        pcall(function() TeleportService:Teleport(placeId) end)
        return
    end

    task.wait(8)
    if env.RunId == myRunId then scanServers(true) end
end

-- Обработка системных ошибок Roblox
task.spawn(function()
    while env.RunId == myRunId do
        if hasError() then
            warn("[AutoHop] Окно ошибки Roblox! Смена сервера через 2 сек...")
            clearErrorPopup()
            task.wait(2)
            if env.RunId ~= myRunId then break end
            env.AttemptId = env.AttemptId + 1
            env.Busy = false
            env.IsHopping = false
            hopNext()
            task.wait(5)
        end
        task.wait(1)
    end
end)

env.FailConn = TeleportService.TeleportInitFailed:Connect(function(p)
    if env.RunId == myRunId and (not Players.LocalPlayer or p == Players.LocalPlayer) then
        env.AttemptId = env.AttemptId + 1
        clearErrorPopup()
        task.wait(2)
        env.Busy = false
        env.IsHopping = false
        hopNext()
    end
end)

if not game:IsLoaded() then game.Loaded:Wait() end
local lp = Players.LocalPlayer or Players.PlayerAdded:Wait()
task.wait(2.5)
currentJobId = game.JobId

local function fetchPage(cursor)
    local urls = {
        "https://games.roblox.com/v1/games/" .. placeId .. "/servers/Public?sortOrder=Asc&excludeFullGames=true&limit=100",
        "https://games.roproxy.com/v1/games/" .. placeId .. "/servers/Public?sortOrder=Asc&excludeFullGames=true&limit=100"
    }
    for _, base in ipairs(urls) do
        local url = cursor and (base .. "&cursor=" .. HttpService:UrlEncode(cursor)) or base
        local ok, res = pcall(function() return HttpService:JSONDecode(game:HttpGet(url)) end)
        if ok and type(res) == "table" and type(res.data) == "table" then
            return res
        end
        task.wait(1)
    end
    return nil
end

scanServers = function(ignoreCache)
    if env.RunId ~= myRunId or env.Busy then return end
    env.Busy = true

    if not ignoreCache then
        local cached = loadCache()
        if cached and #cached > 0 then
            env.Pool = cached
            hopNext()
            return
        end
    end

    local list = {}
    local cursor = nil
    local pages = 0

    repeat
        local data = fetchPage(cursor)
        if not data then
            warn("[AutoHop] Лимит API, ждем 12 сек...")
            task.wait(12)
            break
        end

        for _, s in ipairs(data.data) do
            if type(s) == "table" and type(s.id) == "string" then
                local pl = tonumber(s.playing) or 0
                local max = tonumber(s.maxPlayers) or 0
                local fps = tonumber(s.fps) or 60
                local ping = tonumber(s.ping) or 100

                if s.id ~= currentJobId and not env.Failed[s.id] then
                    if pl >= 1 and pl < MAX_PLAYERS and (max - pl) >= 2 and fps >= 30 and ping <= 450 then
                        table.insert(list, { id = s.id, pl = pl })
                    end
                end
            end
        end

        pages = pages + 1
        cursor = data.nextPageCursor
        if #list >= 15 or not cursor or pages >= 3 then break end
        task.wait(0.3)
    until false

    table.sort(list, function(a, b) return a.pl > b.pl end)

    env.Pool = {}
    for _, item in ipairs(list) do table.insert(env.Pool, item.id) end

    if #env.Pool > 0 then
        print("[AutoHop] Собрано серверов: " .. #env.Pool)
        saveCache()
        hopNext()
    else
        warn("[AutoHop] Подходящих серверов нет, повтор через 10 сек...")
        task.wait(10)
        env.Busy = false
        if env.RunId == myRunId then scanServers(true) end
    end
end

local function checkCount()
    if env.RunId == myRunId and not env.Busy then
        local count = #Players:GetPlayers()
        if count > MAX_PLAYERS then
            warn("[AutoHop] На сервере " .. count .. " игроков (лимит: " .. MAX_PLAYERS .. ")! Меняем сервер...")
            scanServers(false)
        else
            print("[AutoHop] Сервер подходит! Игроков: " .. count .. "/" .. MAX_PLAYERS)
        end
    end
end

checkCount()
env.JoinConn = Players.PlayerAdded:Connect(function()
    task.wait(0.5)
    checkCount()
end)