--[[ Ospei Loader (Module Picker Edition) Shows a small picker box on execution so you can load the QB aimbot, the catching magnets, or both. Uses the shared Ospei UI palette. Optimized: cloneref-wrapped services, safe HTTP wrapper (never throws), deduped reload logic, trim helper, no stacked poll loops / chat handlers when re-executed, cache-first boot per module, stale cache validation, base64 fallback, integrity check hook. ]] local VERSION_URL = "https://ospei-loader.vercel.app/api/version" local CHANGELOG_URL = "https://ospei-loader.vercel.app/api/changelog" local AIMBOT_URL = "https://ospei-loader.vercel.app/api/aimbot" local MAGNETS_URL = "https://ospei-loader.vercel.app/api/magnets" -- Loader build version shown in the footer. Increment per release: L1, L2, ... local LOADER_VERSION = "L1.7" -- The loader fetches whatever /api/aimbot serves (the QB aimbot build) and -- whatever /api/magnets serves (magnets.lua). Optional per-module SHA-256 -- hash endpoints for integrity checking; nil skips the check. local AIMBOT_HASH_URL = "https://ospei-loader.vercel.app/api/hash" local MAGNETS_HASH_URL = nil -- How often (seconds) the background updater re-checks the version. local POLL_INTERVAL = 120 -- Ospei shared UI palette (matches magnets.lua + aimbot StatsPill) local PALETTE = { OuterBg = Color3.fromRGB(16, 17, 21), InnerBg = Color3.fromRGB(26, 28, 34), OuterStroke = Color3.fromRGB(42, 45, 55), InnerStroke = Color3.fromRGB(36, 39, 48), Muted = Color3.fromRGB(169, 173, 187), Accent = Color3.fromRGB(160, 205, 255), TrackBg = Color3.fromRGB(20, 22, 28), TrackHover = Color3.fromRGB(28, 30, 38), White = Color3.new(1, 1, 1), Success = Color3.fromRGB(106, 220, 130), Danger = Color3.fromRGB(230, 110, 110), } local function secureGetService(serviceName) return (cloneref and cloneref(game:GetService(serviceName))) or game:GetService(serviceName) end local TweenService = secureGetService("TweenService") local RunService = secureGetService("RunService") local UserInputService = secureGetService("UserInputService") local Players = secureGetService("Players") local LocalPlayer = Players.LocalPlayer local CoreGui = (type(gethui) == "function" and gethui()) or secureGetService("CoreGui") local env = (getgenv and getgenv()) or _G local req = (syn and syn.request) or (http and http.request) or http_request or (fluxus and fluxus.request) or request if not req then return print("[Ospei Loader] No HTTP request function found.") end -- Safe GET wrapper: never throws, returns the response table or nil local function httpGet(url) local ok, res = pcall(req, {Url = url, Method = "GET"}) if ok and res and res.StatusCode == 200 and res.Body then return res end print("[Ospei Loader] HTTP GET failed for: " .. tostring(url)) if ok then print("[Ospei Loader] Response status: " .. tostring(res and res.StatusCode)) print("[Ospei Loader] Has body: " .. tostring(res and res.Body ~= nil)) else print("[Ospei Loader] Request threw: " .. tostring(res)) end return nil end local function trim(s) return (s and s:match("^%s*(.-)%s*$")) or nil end -- SHA-256 availability check (executor-specific; skip integrity if unavailable) local function getHash(content) if syn and syn.crypt and syn.crypt.hash then return syn.crypt.hash(content, "sha256") elseif crypt and crypt.sha256 then return crypt.sha256(content) end return nil end local function base64Decode(body) if not body then return nil end -- Sniff first: raw Lua source contains characters outside the base64 -- alphabet (spaces, dashes, parens, quotes, ...). If we find any after -- stripping whitespace, the API served plain source — pass it through -- untouched. This prevents lenient executor decoders from "successfully" -- mangling raw Lua into garbage, which then fails to compile at line 1 -- with "expected identifier". local stripped = body:gsub("%s", "") if stripped:find("[^%w%+/=]") then return body end if syn and syn.crypt and syn.crypt.base64 and syn.crypt.base64.decode then local ok, result = pcall(syn.crypt.base64.decode, body) if ok then return result end end if crypt and crypt.base64decode then local ok, result = pcall(crypt.base64decode, body) if ok then return result end end -- Body is already raw Lua (no encoding) — pass through return body end local UpdatePromptGui = nil local VERSION_FILE = "ospei_version.txt" local AUTO_UPDATE_FILE = "ospei_autoupdate.txt" local currentRunningVersion = nil local ShowPermanentToast = nil -- Modules the loader can boot. Each has its own URL, optional hash URL, and -- its own cache files so aimbot and magnets never clobber each other. local MODULES = { aimbot = { Label = "QB Aimbot", Desc = "Route solver + trajectory", Url = AIMBOT_URL, HashUrl = AIMBOT_HASH_URL, CacheFile = "ospei_script_cache.txt", CacheVerFile = "ospei_cache_version.txt", }, magnets = { Label = "Catching Magnets", Desc = "Ball magnet + magnification", Url = MAGNETS_URL, HashUrl = MAGNETS_HASH_URL, CacheFile = "ospei_magnets_cache.txt", CacheVerFile = "ospei_magnets_cache_version.txt", }, } -- Which modules have actually been booted this session (for reloads). local loadedModules = {} local function createUI(className, properties, parent) local obj = Instance.new(className) for k, v in pairs(properties) do obj[k] = v end if parent then obj.Parent = parent end return obj end local function getLocalVersion() if isfile and isfile(VERSION_FILE) then local ok, content = pcall(readfile, VERSION_FILE) if ok then return trim(content) end end return nil end local function saveLocalVersion(ver) if writefile then pcall(writefile, VERSION_FILE, ver) end end local function isAutoUpdatePermanent() if isfile and isfile(AUTO_UPDATE_FILE) then local ok, content = pcall(readfile, AUTO_UPDATE_FILE) return ok and trim(content) == "true" end return false end local function saveCachedScript(mod, content, version) if writefile then pcall(writefile, mod.CacheFile, content) if version then pcall(writefile, mod.CacheVerFile, version) end end end -- Returns cached script only if it matches the provided version string. -- Pass nil to skip version validation (returns raw cache regardless). local function getCachedScript(mod, expectedVersion) if not (isfile and isfile(mod.CacheFile)) then return nil end if expectedVersion then if isfile(mod.CacheVerFile) then local ok, cachedVer = pcall(readfile, mod.CacheVerFile) if not ok or trim(cachedVer) ~= expectedVersion then return nil -- stale cache; caller must download fresh end else return nil -- no version tag; treat as stale end end local ok, content = pcall(readfile, mod.CacheFile) return ok and content or nil end -- Fetches one module's source: cache-first, then network with integrity check. -- Returns raw Lua source or nil on hard failure. local function fetchModule(mod) print("[Ospei Loader] Fetching " .. mod.Label .. " from " .. tostring(mod.Url)) local response = httpGet(mod.Url) if not response then print("[Ospei Loader] Network fetch failed for " .. mod.Label .. ", trying cache...") local cached = getCachedScript(mod, nil) if cached then ShowPermanentToast(mod.Label .. " Load Failed", "Could not download. Using cached version.") return cached end print("[Ospei Loader] No cache available for " .. mod.Label) ShowPermanentToast(mod.Label .. " Load Failed", "Could not download and no cache available.") return nil end local decoded = base64Decode(response.Body) if not decoded then print("[Ospei Loader] Failed to decode response for " .. mod.Label) ShowPermanentToast(mod.Label .. " Load Failed", "Could not decode response.") return nil end -- Integrity check (only if the module defines a hash endpoint) if mod.HashUrl then local hashResp = httpGet(mod.HashUrl) if hashResp then local expectedHash = trim(hashResp.Body) local actualHash = getHash(decoded) if actualHash and expectedHash and actualHash ~= expectedHash then print("[Ospei Loader] Integrity check failed for " .. mod.Label .. " — script hash mismatch.") local cached = getCachedScript(mod, nil) if cached then ShowPermanentToast("Integrity Check Failed", mod.Label .. " hash mismatch — using cached version.") return cached else ShowPermanentToast("Integrity Check Failed", mod.Label .. " hash mismatch — load aborted.") return nil end end end end local loadFunc = loadstring or load local loadedFunction, err = loadFunc(decoded) if not loadedFunction then print("[Ospei Loader] Failed to compile " .. mod.Label .. ":", err) local cached = getCachedScript(mod, nil) if cached then ShowPermanentToast("Compile Error", mod.Label .. " failed to compile — using cached version.") return cached end ShowPermanentToast("Compile Error", "Could not compile " .. mod.Label .. ": " .. tostring(err)) return nil end saveCachedScript(mod, decoded, currentRunningVersion) return decoded end -- Compiles + spawns one module and records it for reloads. local function loadModule(modId, statusLabel) local mod = MODULES[modId] if not mod then return false end local content = fetchModule(mod) if not content then return false end -- Create a custom environment for the loaded module. Fall back to the -- loader's real environment so executor globals (getgenv, _G, task, ...) -- keep working inside the module. local moduleEnv = setmetatable({}, { __index = getfenv() }) for k, v in pairs(env) do moduleEnv[k] = v -- Copy global environment end -- Proxy the 'status' object if it exists in the main picker's UI scope if statusLabel then local proxiedStatus = { _originalText = "", _moduleLabel = mod.Label, _pickerStatusLabel = statusLabel, } setmetatable(proxiedStatus, { __index = function(t, k) if k == "Text" then return t._originalText elseif k == "Name" then -- Module might check its own name return t._moduleLabel else return rawget(t, k) or t._pickerStatusLabel[k] -- Fallback to pickerStatusLabel properties end end, __newindex = function(t, k, v) if k == "Text" then t._originalText = v -- Update the picker's main statusLabel with module-specific text t._pickerStatusLabel.Text = t._moduleLabel .. ": " .. v elseif k == "Name" then rawset(t, k, v) else t._pickerStatusLabel[k] = v -- Pass other property changes to the actual TextLabel end end, }) moduleEnv.status = proxiedStatus end local loadedFunction, err if loadstring then loadedFunction, err = loadstring(content) if loadedFunction and setfenv then local setOk, setErr = pcall(setfenv, loadedFunction, moduleEnv) if not setOk then print("[Ospei Loader] setfenv failed for " .. mod.Label .. ", falling back to global env:", tostring(setErr)) end end else loadedFunction, err = nil, "No script loader available" end if not loadedFunction then ShowPermanentToast("Compile Error", "Could not compile " .. mod.Label .. ": " .. tostring(err)) return false end task.spawn(function() -- Set initial status before running the module if statusLabel then statusLabel.Text = "Loading " .. mod.Label .. "..." end local ok, err_msg = pcall(loadedFunction) if not ok then print("[Ospei Loader] Error running module " .. mod.Label .. ":", err_msg) if statusLabel then statusLabel.Text = mod.Label .. " failed: " .. tostring(err_msg) end else print("[Ospei Loader] Module " .. mod.Label .. " loaded successfully") end end) loadedModules[modId] = true return true end ------------------------------------------------------------------------------- -- MODULE PICKER UI ------------------------------------------------------------------------------- local PickerGui = nil local PickerFrame = nil local PickerScale = nil -- Idle auto-unload: the picker unloads itself if the player hasn't loaded a -- script within AUTO_UNLOAD_DELAY seconds. After a successful load the block -- locks green for COOLDOWN_DURATION (cooldown), then the green fades out. local AUTO_UNLOAD_DELAY = 15 local COOLDOWN_DURATION = 4.5 local UPDATE_PROMPT_ICON = "rbxassetid://125122443050907" local pendingUpdatePush = false local pickerButtons = {} local autoUnloadTask = nil local function cancelAutoUnload() if autoUnloadTask then task.cancel(autoUnloadTask) autoUnloadTask = nil end end local slideOutPicker local dragConn slideOutPicker = function(isAutoUnload) cancelAutoUnload() local gui, frame, scale = PickerGui, PickerFrame, PickerScale PickerGui = nil PickerFrame = nil PickerScale = nil if dragConn then dragConn:Disconnect() dragConn = nil end if not gui or not frame or not frame.Parent then return end if isAutoUnload then -- Implement opacity pulse here before sliding out TweenService:Create(frame, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {BackgroundTransparency = 0.5}):Play() task.wait(0.2) TweenService:Create(frame, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {BackgroundTransparency = 0}):Play() task.wait(0.2) end local outT = TweenInfo.new(0.28, Enum.EasingStyle.Quad, Enum.EasingDirection.In) TweenService:Create(frame, outT, {Position = UDim2.new(0.5, 0, 0.5, 40), BackgroundTransparency = 1}):Play() TweenService:Create(scale, outT, {Scale = 0.92}):Play() task.wait(0.28) if gui and gui.Parent then gui:Destroy() end end local function scheduleAutoUnload() cancelAutoUnload() autoUnloadTask = task.delay(AUTO_UNLOAD_DELAY, function() autoUnloadTask = nil slideOutPicker(true) end) end -- Update-push helpers: flag picker blocks that need a reload-for-update. local function markUpdatePush() pendingUpdatePush = true for _, entry in ipairs(pickerButtons) do entry.needsUpdate = true entry.updateIcon.Visible = true end end local function clearUpdateIcon(btn) for _, entry in ipairs(pickerButtons) do if entry.btn == btn then entry.needsUpdate = false entry.updateIcon.Visible = false entry.updateIcon.Rotation = 0 entry.updateIconScale.Scale = 1 break end end pendingUpdatePush = false for _, entry in ipairs(pickerButtons) do if entry.needsUpdate then pendingUpdatePush = true break end end end local function clearAllUpdateIcons() for _, entry in ipairs(pickerButtons) do entry.needsUpdate = false entry.updateIcon.Visible = false entry.updateIcon.Rotation = 0 entry.updateIconScale.Scale = 1 end pendingUpdatePush = false end local PickerLocked = false local LOADING_ICON = "rbxassetid://139478826975785" local DOWNLOAD_ICON = "rbxassetid://97655061272340" local DOWNLOAD_ICON_HOVER = "rbxassetid://100697582291089" -- Nice download animation: the block's icon becomes a loading spinner that -- keeps spinning while the module boots. On success the block locks into a -- green "loaded" state for COOLDOWN_DURATION (cooldown), then the green fades -- out — signifying the block can be loaded/reloaded again. The picker stays -- open; it unloads itself after AUTO_UNLOAD_DELAY of no loading. local function loadSelection(ids, statusLabel, btn, icon, chip, stroke, title, onSuccess) if PickerLocked then return end PickerLocked = true cancelAutoUnload() local btnScale = btn and btn:FindFirstChildOfClass("UIScale") -- press the block if btnScale then TweenService:Create(btnScale, TweenInfo.new(0.1, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {Scale = 0.96}):Play() end -- swap the download icon for the loading spinner and spin it local spinning = false if icon then spinning = true icon.Image = LOADING_ICON icon.ImageTransparency = 0 icon.ImageColor3 = PALETTE.White icon.Size = UDim2.fromOffset(22, 22) icon.Rotation = 0 task.spawn(function() while spinning and icon and icon.Parent do local t = TweenService:Create(icon, TweenInfo.new(0.6, Enum.EasingStyle.Linear), {Rotation = icon.Rotation + 360}) t:Play() t.Completed:Wait() end end) end if statusLabel then statusLabel.Text = "Downloading..." end task.spawn(function() task.wait(0.35) if btnScale then TweenService:Create(btnScale, TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.In), {Scale = 1.0}):Play() end local successfulLoads = 0 for _, id in ipairs(ids) do if loadedModules[id] then successfulLoads = successfulLoads + 1 else if statusLabel then statusLabel.Text = "Loading " .. MODULES[id].Label .. "..." end if loadModule(id, statusLabel) then successfulLoads = successfulLoads + 1 end task.wait(0.05) end end spinning = false -- Load failed (no cache, hash mismatch, etc.) — release the lock, -- restore the icon, and reschedule the auto-unload so the picker -- can still be used. if successfulLoads < #ids then icon.Image = DOWNLOAD_ICON icon.ImageTransparency = 0.4 icon.ImageColor3 = PALETTE.Muted icon.Size = UDim2.fromOffset(20, 20) icon.Rotation = 0 PickerLocked = false scheduleAutoUnload() if statusLabel then statusLabel.Text = "" end -- Don't return if some modules loaded; proceed to success state if successfulLoads == 0 then return end end -- Success: persistent green "loaded" state (cooldown starts here) btn:SetAttribute("Loaded", true) icon.Image = DOWNLOAD_ICON icon.ImageTransparency = 0 icon.ImageColor3 = PALETTE.Success icon.Size = UDim2.fromOffset(20, 20) icon.Rotation = 0 stroke.Color = PALETTE.Success stroke.Transparency = 0 stroke.Thickness = 1.5 if chip then chip.BackgroundColor3 = PALETTE.Success chip.BackgroundTransparency = 0.25 end if statusLabel then statusLabel.Text = "Running — " .. title end -- Notify the caller (e.g. clear the reload-for-update icon) if onSuccess then onSuccess() end -- Cooldown: green holds for COOLDOWN_DURATION, then fades out task.wait(COOLDOWN_DURATION) if not btn or not btn.Parent then return end local fade = TweenInfo.new(0.8, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) TweenService:Create(stroke, fade, { Color = btn:GetAttribute("Accent") and PALETTE.Accent or PALETTE.OuterStroke, Transparency = btn:GetAttribute("Accent") and 0.35 or 0, Thickness = 1, }):Play() TweenService:Create(icon, fade, { ImageColor3 = PALETTE.Muted, ImageTransparency = 0.4, Size = UDim2.fromOffset(20, 20), Rotation = 0, }):Play() if chip then TweenService:Create(chip, fade, { BackgroundColor3 = PALETTE.TrackBg, BackgroundTransparency = 0.85, }):Play() end task.wait(0.8) btn:SetAttribute("Loaded", false) PickerLocked = false if statusLabel then statusLabel.Text = "" end -- Idle countdown restarts after a successful load scheduleAutoUnload() end) end local function ShowPicker() if PickerGui and PickerGui.Parent then return end PickerLocked = false -- Fresh picker: clear stale button refs before the new ones register table.clear(pickerButtons) local gui = createUI("ScreenGui", {DisplayOrder = 999, IgnoreGuiInset = true, ResetOnSpawn = false}, CoreGui) local frame = createUI("Frame", { Size = UDim2.fromOffset(290, 245), AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0.5, 0, 0.5, 40), BackgroundColor3 = PALETTE.OuterBg, BackgroundTransparency = 0, }, gui) createUI("UICorner", {CornerRadius = UDim.new(0, 14)}, frame) createUI("UIStroke", {Color = PALETTE.OuterStroke, Thickness = 1.5}, frame) -- Mobile-friendly: scale the whole picker to fit the viewport, so the box -- isn't oversized on small phones or undersized on large monitors. local camera = workspace.CurrentCamera local viewportSize = camera and camera.ViewportSize or Vector2.new(800, 600) local baseW, baseH = 290, 245 local fitScale = math.min(viewportSize.X / baseW, viewportSize.Y / baseH) local pickerScale = math.clamp(fitScale, 0.75, 1.15) local scale = Instance.new("UIScale") scale.Scale = pickerScale * 0.94 scale.Parent = frame local inner = createUI("Frame", { Size = UDim2.new(1, -12, 1, -12), Position = UDim2.new(0.5, 0, 0.5, 0), AnchorPoint = Vector2.new(0.5, 0.5), BackgroundColor3 = PALETTE.InnerBg, BorderSizePixel = 0, ClipsDescendants = true, }, frame) createUI("UICorner", {CornerRadius = UDim.new(0, 10)}, inner) createUI("UIStroke", {Color = PALETTE.InnerStroke, Thickness = 1}, inner) -- Header createUI("ImageLabel", { Name = "TitleLogo", Size = UDim2.fromOffset(20, 20), Position = UDim2.new(0, 14, 0, 10), BackgroundTransparency = 1, Image = "rbxassetid://71068731540117", ScaleType = Enum.ScaleType.Fit, ZIndex = 3, }, inner) createUI("TextLabel", { Size = UDim2.new(1, 0, 0, 20), Position = UDim2.new(0, 42, 0, 10), BackgroundTransparency = 1, Font = Enum.Font.GothamBold, Text = "Ospei Loader", TextColor3 = PALETTE.White, TextSize = 14, TextXAlignment = Enum.TextXAlignment.Left, }, inner) createUI("TextLabel", { Size = UDim2.new(1, 0, 0, 14), Position = UDim2.new(0, 14, 0, 30), BackgroundTransparency = 1, Font = Enum.Font.Gotham, Text = "Select what to load", TextColor3 = PALETTE.Muted, TextSize = 10, TextXAlignment = Enum.TextXAlignment.Left, }, inner) -- Right-aligned control strip. Kept narrow (right corner only) so the drag -- hit target below still covers the rest of the header without overlap. local CONTROLS_W = 76 -- 24 discord + 6 + 20 minimize + 6 + 20 close local headerControls = createUI("Frame", { Name = "HeaderControls", Size = UDim2.fromOffset(CONTROLS_W, 56), Position = UDim2.new(1, -14, 0, 0), AnchorPoint = Vector2.new(1, 0), BackgroundTransparency = 1, }, inner) local controlsLayout = Instance.new("UIListLayout") controlsLayout.FillDirection = Enum.FillDirection.Horizontal controlsLayout.HorizontalAlignment = Enum.HorizontalAlignment.Left controlsLayout.VerticalAlignment = Enum.VerticalAlignment.Center controlsLayout.Padding = UDim.new(0, 6) controlsLayout.SortOrder = Enum.SortOrder.LayoutOrder controlsLayout.Parent = headerControls local function makeHeaderButton(assetId, name, layoutOrder) local btn = createUI("ImageButton", { Name = name, Size = UDim2.fromOffset(20, 20), LayoutOrder = layoutOrder, BackgroundTransparency = 1, Image = assetId, ScaleType = Enum.ScaleType.Fit, ZIndex = 7, AutoButtonColor = false, }, headerControls) local btnScale = Instance.new("UIScale", btn) return btn, btnScale end local closeBtn, closeScale = makeHeaderButton("rbxassetid://104513507325960", "CloseBtn", 4) local minimizeBtn, minimizeScale = makeHeaderButton("rbxassetid://119519205991205", "MinimizeBtn", 3) local DISCORD_CODE = "dB69Emv8MS" local discordIcon = createUI("ImageButton", { Name = "DiscordIcon", Size = UDim2.fromOffset(24, 24), LayoutOrder = 2, BackgroundTransparency = 1, Image = "rbxassetid://81937540990844", ScaleType = Enum.ScaleType.Fit, ZIndex = 7, AutoButtonColor = false, }, headerControls) local discordScale = Instance.new("UIScale", discordIcon) local minimized = false -- Full-card hit area, only active while minimized: clicking anywhere on -- the tiny card restores it to full size. local restoreHit = createUI("TextButton", { Name = "RestoreHit", Size = UDim2.new(1, 0, 1, 0), BackgroundTransparency = 1, Text = "", Visible = false, ZIndex = 12, }, frame) local function setMinimized(state) minimized = state local t = TweenInfo.new(0.3, Enum.EasingStyle.Quart, Enum.EasingDirection.Out) if state then TweenService:Create(scale, t, {Scale = 0.35}):Play() TweenService:Create(frame, t, {Position = UDim2.new(1, -70, 1, -45)}):Play() restoreHit.Visible = true else TweenService:Create(scale, t, {Scale = pickerScale}):Play() TweenService:Create(frame, t, {Position = UDim2.new(0.5, 0, 0.5, 0)}):Play() restoreHit.Visible = false end end minimizeBtn.MouseButton1Click:Connect(function() setMinimized(not minimized) end) closeBtn.MouseButton1Click:Connect(function() slideOutPicker(false) end) -- Uniform hover affordance for both window buttons local function wireButtonHover(btn, btnScale, hoverTint) btn.MouseEnter:Connect(function() TweenService:Create(btnScale, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {Scale = 1.15}):Play() if hoverTint then TweenService:Create(btn, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {ImageColor3 = hoverTint}):Play() end end) btn.MouseLeave:Connect(function() TweenService:Create(btnScale, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {Scale = 1}):Play() if hoverTint then TweenService:Create(btn, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {ImageColor3 = PALETTE.White}):Play() end end) end wireButtonHover(minimizeBtn, minimizeScale) wireButtonHover(closeBtn, closeScale, PALETTE.Danger) -- Tooltip (invite code) — two-layer mini card matching the main UI palette. -- Lives on the ScreenGui so it renders above the card. local tooltipOuter = createUI("Frame", { Name = "DiscordTooltipOuter", Size = UDim2.fromOffset(136, 42), BackgroundColor3 = PALETTE.OuterBg, BackgroundTransparency = 0, ZIndex = 20, Visible = false, }, gui) createUI("UICorner", {CornerRadius = UDim.new(0, 9)}, tooltipOuter) createUI("UIStroke", {Color = PALETTE.OuterStroke, Thickness = 1.5}, tooltipOuter) local tooltip = createUI("Frame", { Size = UDim2.new(1, -6, 1, -6), Position = UDim2.new(0.5, 0, 0.5, 0), AnchorPoint = Vector2.new(0.5, 0.5), BackgroundColor3 = PALETTE.InnerBg, BorderSizePixel = 0, ClipsDescendants = false, }, tooltipOuter) createUI("UICorner", {CornerRadius = UDim.new(0, 7)}, tooltip) createUI("UIStroke", {Color = PALETTE.InnerStroke, Thickness = 1}, tooltip) local tooltipScale = Instance.new("UIScale", tooltipOuter) tooltipScale.Scale = 0.5 local tooltipCode = createUI("TextButton", { Size = UDim2.new(1, 0, 1, 0), BackgroundTransparency = 1, Font = Enum.Font.GothamBold, Text = DISCORD_CODE, TextColor3 = PALETTE.White, TextSize = 12, ZIndex = 21, }, tooltip) local tooltipVisible = false local function copyToClipboard(text) local ok = false if setclipboard then ok = pcall(setclipboard, text) end if not ok and toclipboard then ok = pcall(toclipboard, text) end if not ok and writeclipboard then ok = pcall(writeclipboard, text) end if not ok and clipboard and clipboard.set then ok = pcall(clipboard.set, text) end return ok end local function positionTooltip(anchorPos) -- input.Position is a Vector3; normalize so the subtraction below -- never receives the wrong vector type. anchorPos = Vector2.new(anchorPos.X, anchorPos.Y) local size = tooltipOuter.AbsoluteSize local pos = anchorPos - Vector2.new(size.X / 2, size.Y + 4) pos = Vector2.new( math.clamp(pos.X, 6, math.max(6, viewportSize.X - size.X - 6)), math.clamp(pos.Y, 6, math.max(6, viewportSize.Y - size.Y - 6)) ) tooltipOuter.Position = UDim2.fromOffset(pos.X, pos.Y) end local function showTooltip(anchorPos) if tooltipVisible then return end tooltipVisible = true tooltipCode.Text = DISCORD_CODE tooltipCode.TextColor3 = PALETTE.White tooltipOuter.Visible = true positionTooltip(anchorPos) tooltipScale.Scale = 0.5 TweenService:Create(tooltipScale, TweenInfo.new(0.3, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {Scale = 1}):Play() end local function hideTooltip() tooltipVisible = false tooltipOuter.Visible = false end -- Tooltip appears above the cursor, then stays put so it can be reached -- and clicked. Hide is delayed + hover-tracked so moving onto the tooltip -- doesn't make it vanish. local mousePosition = Vector2.zero local iconHovered = false local tooltipHovered = false local hideScheduled = false local function scheduleHide() if hideScheduled then return end hideScheduled = true task.delay(0.15, function() hideScheduled = false if not iconHovered and not tooltipHovered then hideTooltip() end end) end UserInputService.InputChanged:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseMovement then mousePosition = Vector2.new(input.Position.X, input.Position.Y) end end) discordIcon.MouseEnter:Connect(function() iconHovered = true TweenService:Create(discordScale, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {Scale = 1.2}):Play() showTooltip(mousePosition) end) discordIcon.MouseLeave:Connect(function() iconHovered = false TweenService:Create(discordScale, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {Scale = 1}):Play() scheduleHide() end) tooltipOuter.MouseEnter:Connect(function() tooltipHovered = true end) tooltipOuter.MouseLeave:Connect(function() tooltipHovered = false scheduleHide() end) tooltip.MouseEnter:Connect(function() tooltipHovered = true end) tooltip.MouseLeave:Connect(function() tooltipHovered = false scheduleHide() end) -- Mobile: no hover, so a tap toggles the tooltip near the finger discordIcon.InputBegan:Connect(function(input) if input.UserInputType == Enum.UserInputType.Touch then if tooltipVisible then hideTooltip() else TweenService:Create(discordScale, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {Scale = 1.2}):Play() task.delay(0.4, function() if not tooltipVisible then TweenService:Create(discordScale, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {Scale = 1}):Play() end end) showTooltip(Vector2.new(input.Position.X, input.Position.Y)) end end end) -- Clicking the code copies it; white -> green + bounce on success tooltipCode.MouseButton1Click:Connect(function() copyToClipboard(DISCORD_CODE) tooltipCode.Text = "Copied!" tooltipCode.TextColor3 = Color3.fromRGB(106, 220, 130) TweenService:Create(tooltipScale, TweenInfo.new(0.25, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {Scale = 1.12}):Play() TweenService:Create(tooltipScale, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {Scale = 1}):Play() task.delay(1.4, function() if tooltipVisible then tooltipCode.Text = DISCORD_CODE tooltipCode.TextColor3 = PALETTE.White end end) end) -- Divider createUI("Frame", { Size = UDim2.new(1, -28, 0, 1), Position = UDim2.new(0, 14, 0, 48), BackgroundColor3 = PALETTE.OuterStroke, BackgroundTransparency = 0.35, BorderSizePixel = 0, }, inner) -- Invisible drag hit target covering the header (minus the control strip) local dragHit = createUI("TextButton", { Name = "DragHit", Size = UDim2.new(1, -(CONTROLS_W + 14), 0, 56), BackgroundTransparency = 1, Text = "", ZIndex = 5, }, inner) -- Buttons local buttonRow = createUI("Frame", { Size = UDim2.new(1, -28, 0, 138), Position = UDim2.new(0, 14, 0, 56), BackgroundTransparency = 1, }, inner) local rowLayout = Instance.new("UIListLayout") rowLayout.FillDirection = Enum.FillDirection.Vertical rowLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center rowLayout.Padding = UDim.new(0, 4) rowLayout.SortOrder = Enum.SortOrder.LayoutOrder rowLayout.Parent = buttonRow local function createModuleButton(text, subText, accent, layoutOrder, onClick) local btn = createUI("TextButton", { Size = UDim2.new(1, 0, 0, 36), LayoutOrder = layoutOrder, AutoButtonColor = false, BackgroundColor3 = accent and PALETTE.TrackHover or PALETTE.TrackBg, Text = "", -- blank: the button is a pure background/container now }, buttonRow) btn:SetAttribute("Accent", accent) createUI("UICorner", {CornerRadius = UDim.new(0, 7)}, btn) local btnStroke = createUI("UIStroke", { Color = accent and PALETTE.Accent or PALETTE.OuterStroke, Transparency = accent and 0.35 or 0, Thickness = 1, }, btn) Instance.new("UIScale", btn) -- Content group: UIPadding aligns title + subtext on the same axis local content = createUI("Frame", { Size = UDim2.new(1, 0, 1, 0), BackgroundTransparency = 1, }, btn) createUI("UIPadding", { PaddingLeft = UDim.new(0, 14), PaddingTop = UDim.new(0, 7), }, content) -- Title label (no more " " padding hack) createUI("TextLabel", { Size = UDim2.new(1, -30, 0, 15), BackgroundTransparency = 1, Font = Enum.Font.GothamBold, Text = text, TextColor3 = accent and PALETTE.Accent or PALETTE.White, TextSize = 12, TextXAlignment = Enum.TextXAlignment.Left, }, content) -- Subtext label, exactly under the title createUI("TextLabel", { Size = UDim2.new(1, -30, 0, 13), Position = UDim2.new(0, 0, 0, 15), BackgroundTransparency = 1, Font = Enum.Font.Gotham, Text = subText, TextColor3 = PALETTE.Muted, TextSize = 9, TextXAlignment = Enum.TextXAlignment.Left, }, content) -- Action icon chip (subtle at rest, bright on hover) + download icon local chip = createUI("Frame", { Size = UDim2.fromOffset(26, 26), Position = UDim2.new(1, -9, 0.5, 0), AnchorPoint = Vector2.new(1, 0.5), BackgroundColor3 = accent and PALETTE.Accent or PALETTE.TrackBg, BackgroundTransparency = 0.85, BorderSizePixel = 0, ZIndex = 3, }, btn) createUI("UICorner", {CornerRadius = UDim.new(0, 8)}, chip) local icon = createUI("ImageLabel", { Size = UDim2.fromOffset(20, 20), Position = UDim2.new(1, -12, 0.5, 0), AnchorPoint = Vector2.new(1, 0.5), BackgroundTransparency = 1, Image = DOWNLOAD_ICON, ImageColor3 = PALETTE.Muted, ImageTransparency = 0.4, ScaleType = Enum.ScaleType.Fit, ZIndex = 4, }, btn) -- Reload-for-update badge: top-center, tilts until reloaded local updateIcon = createUI("ImageLabel", { Name = "UpdatePromptIcon", Size = UDim2.fromOffset(22, 22), Position = UDim2.new(0.5, 0, 0, 0), AnchorPoint = Vector2.new(0.5, 0), BackgroundTransparency = 1, Image = UPDATE_PROMPT_ICON, ScaleType = Enum.ScaleType.Fit, ZIndex = 10, Visible = false, }, btn) local updateIconScale = Instance.new("UIScale", updateIcon) task.spawn(function() while updateIcon and updateIcon.Parent do if updateIcon.Visible then TweenService:Create(updateIcon, TweenInfo.new(0.3, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut), {Rotation = 12}):Play() TweenService:Create(updateIconScale, TweenInfo.new(0.3, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut), {Scale = 1.2}):Play() task.wait(0.6) TweenService:Create(updateIcon, TweenInfo.new(0.3, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut), {Rotation = -12}):Play() TweenService:Create(updateIconScale, TweenInfo.new(0.3, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut), {Scale = 0.9}):Play() task.wait(0.6) else task.wait(0.2) end end end) -- Register for update-push highlighting local entry = { btn = btn, chip = chip, stroke = btnStroke, icon = icon, updateIcon = updateIcon, updateIconScale = updateIconScale, needsUpdate = false, } table.insert(pickerButtons, entry) btn.MouseEnter:Connect(function() TweenService:Create(btn, TweenInfo.new(0.15), {BackgroundColor3 = PALETTE.TrackHover}):Play() if btn:GetAttribute("Loaded") then TweenService:Create(chip, TweenInfo.new(0.15), {BackgroundTransparency = 0.15}):Play() else TweenService:Create(chip, TweenInfo.new(0.15), {BackgroundTransparency = 0.6}):Play() end if not PickerLocked and not btn:GetAttribute("Loaded") then icon.Image = DOWNLOAD_ICON_HOVER TweenService:Create(icon, TweenInfo.new(0.18, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), { ImageTransparency = 0.1, ImageColor3 = PALETTE.White, Size = UDim2.fromOffset(23, 23), Rotation = -8, }):Play() end end) btn.MouseLeave:Connect(function() TweenService:Create(btn, TweenInfo.new(0.15), {BackgroundColor3 = accent and PALETTE.TrackHover or PALETTE.TrackBg}):Play() if btn:GetAttribute("Loaded") then TweenService:Create(chip, TweenInfo.new(0.15), {BackgroundTransparency = 0.25}):Play() else TweenService:Create(chip, TweenInfo.new(0.15), {BackgroundTransparency = 0.85}):Play() end if not PickerLocked and not btn:GetAttribute("Loaded") then icon.Image = DOWNLOAD_ICON TweenService:Create(icon, TweenInfo.new(0.18, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), { ImageTransparency = 0.4, ImageColor3 = PALETTE.Muted, Size = UDim2.fromOffset(20, 20), Rotation = 0, }):Play() end end) btn.MouseButton1Click:Connect(function() onClick(btn, icon, chip, btnStroke, updateIcon) end) return btn end local statusLabel = createUI("TextLabel", { Size = UDim2.new(1, -28, 0, 14), Position = UDim2.new(0, 14, 0, 198), BackgroundTransparency = 1, Font = Enum.Font.Gotham, Text = "", TextColor3 = PALETTE.Muted, TextSize = 9, TextXAlignment = Enum.TextXAlignment.Center, }, inner) -- Footer: version (left) and keybind hint (right). No divider — the -- padding above is enough separation. createUI("TextLabel", { Size = UDim2.new(0.5, -14, 0, 14), Position = UDim2.new(0, 14, 0, 217), BackgroundTransparency = 1, Font = Enum.Font.Gotham, Text = "OPSEI LOADER • v" .. LOADER_VERSION, TextColor3 = PALETTE.Muted, TextSize = 8, TextXAlignment = Enum.TextXAlignment.Left, }, inner) createUI("TextLabel", { Size = UDim2.new(0.5, -14, 0, 14), Position = UDim2.new(1, -14, 0, 217), AnchorPoint = Vector2.new(1, 0), BackgroundTransparency = 1, Font = Enum.Font.Gotham, Text = "[RightShift] Hide/Show", TextColor3 = PALETTE.Muted, TextSize = 8, TextXAlignment = Enum.TextXAlignment.Right, }, inner) -- RightShift toggles the whole picker UserInputService.InputBegan:Connect(function(input) if input.KeyCode == Enum.KeyCode.RightShift then gui.Enabled = not gui.Enabled end end) createModuleButton("QB Aimbot", "Route solver, lead, trajectory", false, 1, function(btn, icon, chip, stroke, updateIcon) loadSelection({"aimbot"}, statusLabel, btn, icon, chip, stroke, "QB Aimbot", function() if pendingUpdatePush then clearUpdateIcon(btn) end end) end) createModuleButton("Catching Magnets", "Ball magnet + magnification", false, 2, function(btn, icon, chip, stroke, updateIcon) loadSelection({"magnets"}, statusLabel, btn, icon, chip, stroke, "Catching Magnets", function() if pendingUpdatePush then clearUpdateIcon(btn) end end) end) createModuleButton("Load Both", "Aimbot + magnets together", true, 3, function(btn, icon, chip, stroke, updateIcon) loadSelection({"aimbot", "magnets"}, statusLabel, btn, icon, chip, stroke, "Load Both", function() if pendingUpdatePush then clearUpdateIcon(btn) end end) end) -- Smooth window drag (header area only) local dragging = false local dragInput = nil local dragStart = nil local startPos = nil local currentSmoothPos = nil local FOLLOW_SPEED = 22 local function beginWindowDrag(input) if input.UserInputType ~= Enum.UserInputType.MouseButton1 and input.UserInputType ~= Enum.UserInputType.Touch then return end dragging = true dragStart = input.Position startPos = frame.Position currentSmoothPos = Vector2.new(startPos.X.Offset, startPos.Y.Offset) input.Changed:Connect(function() if input.UserInputState == Enum.UserInputState.End then dragging = false end end) end dragHit.InputBegan:Connect(beginWindowDrag) dragHit.InputChanged:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then dragInput = input end end) restoreHit.MouseButton1Click:Connect(function() setMinimized(false) end) dragConn = RunService.RenderStepped:Connect(function(dt) if dragging and dragInput then local delta = dragInput.Position - dragStart local desired = Vector2.new(startPos.X.Offset + delta.X, startPos.Y.Offset + delta.Y) -- Keep the picker on screen (esp. mobile touch drags) local halfW = frame.AbsoluteSize.X * pickerScale / 2 local halfH = frame.AbsoluteSize.Y * pickerScale / 2 desired = Vector2.new( math.clamp(desired.X, -viewportSize.X / 2 + halfW, viewportSize.X / 2 - halfW), math.clamp(desired.Y, -viewportSize.Y / 2 + halfH, viewportSize.Y / 2 - halfH) ) local t = 1 - math.exp(-FOLLOW_SPEED * dt) currentSmoothPos = currentSmoothPos:Lerp(desired, t) frame.Position = UDim2.new(startPos.X.Scale, currentSmoothPos.X, startPos.Y.Scale, currentSmoothPos.Y) end end) PickerGui = gui PickerFrame = frame PickerScale = scale -- Start the idle countdown (buttons already registered above) scheduleAutoUnload() -- Animate in local inT = TweenInfo.new(0.4, Enum.EasingStyle.Quart, Enum.EasingDirection.Out) TweenService:Create(frame, inT, { Position = UDim2.new(0.5, 0, 0.5, 0), }):Play() TweenService:Create(scale, inT, {Scale = pickerScale}):Play() end ------------------------------------------------------------------------------- -- TOASTS & CHAT COMMAND ------------------------------------------------------------------------------- -- Bottom-left toast for chat command feedback local permanentToastGui = nil ShowPermanentToast = function(titleText, descText) if permanentToastGui and permanentToastGui.Parent then permanentToastGui:Destroy() end local toastGui = createUI("ScreenGui", {DisplayOrder = 999, IgnoreGuiInset = true, ResetOnSpawn = false}, CoreGui) permanentToastGui = toastGui local mainFrame = createUI("Frame", { Size = UDim2.fromOffset(320, 120), AnchorPoint = Vector2.new(0, 1), Position = UDim2.new(0, -350, 1, -20), BackgroundColor3 = PALETTE.OuterBg, BorderSizePixel = 0, BackgroundTransparency = 1, }, toastGui) createUI("UICorner", {CornerRadius = UDim.new(0, 8)}, mainFrame) createUI("UIStroke", {Color = PALETTE.OuterStroke}, mainFrame) local toastTitle = createUI("TextLabel", { Size = UDim2.new(1, 0, 0, 35), BackgroundColor3 = PALETTE.InnerBg, Text = " " .. titleText, Font = Enum.Font.GothamBold, TextColor3 = PALETTE.White, TextSize = 13, TextXAlignment = Enum.TextXAlignment.Left, }, mainFrame) createUI("UICorner", {CornerRadius = UDim.new(0, 8)}, toastTitle) createUI("TextLabel", { Size = UDim2.new(1, -20, 0, 60), Position = UDim2.new(0.5, 0, 0, 42), AnchorPoint = Vector2.new(0.5, 0), BackgroundTransparency = 1, Font = Enum.Font.Gotham, TextColor3 = PALETTE.Muted, TextSize = 12, TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Top, TextWrapped = true, Text = descText, }, mainFrame) TweenService:Create(mainFrame, TweenInfo.new(0.4, Enum.EasingStyle.Quart, Enum.EasingDirection.Out), { Position = UDim2.new(0, 20, 1, -20), BackgroundTransparency = 0, }):Play() task.delay(4.5, function() if mainFrame and mainFrame.Parent then TweenService:Create(mainFrame, TweenInfo.new(0.3), {Position = UDim2.new(0, -350, 1, -20)}):Play() task.wait(0.3) toastGui:Destroy() permanentToastGui = nil end end) end -- /update chat handler — reconnect-only-once so re-executing this file -- never stacks duplicate listeners. local CHAT_CONN_NAME = "OspeiLoaderChatted" if env[CHAT_CONN_NAME] then pcall(function() env[CHAT_CONN_NAME]:Disconnect() end) end env[CHAT_CONN_NAME] = LocalPlayer.Chatted:Connect(function(msg) if trim(msg:lower()) == "/update" then local now = tick() local last = env.OspeiLastUpdateAt or 0 if now - last < 1 then return end env.OspeiLastUpdateAt = now if writefile then pcall(writefile, AUTO_UPDATE_FILE, "true") end ShowPermanentToast("Background Auto-Updates", "Forever on! Background updates are now permanently enabled.") end end) ------------------------------------------------------------------------------- -- UPDATE HANDLING ------------------------------------------------------------------------------- -- Reloads whatever modules are currently booted (best-effort cleanup first). local function ReloadLoadedModules() local ids = {} for id, _ in pairs(loadedModules) do table.insert(ids, id) end if #ids == 0 then return end if env.Ospei_Cleanup then pcall(env.Ospei_Cleanup) end if env.Ospei_MagnetsCleanup then pcall(env.Ospei_MagnetsCleanup) end task.wait(0.1) table.clear(loadedModules) for _, id in ipairs(ids) do loadModule(id) task.wait(0.05) end end env.ReloadScript = function() ReloadLoadedModules() -- A reload-for-update succeeded: dismiss the update badges clearAllUpdateIcons() end local function ShowUpdatePrompt(newVersion, changelogText) if UpdatePromptGui and UpdatePromptGui.Parent then return end local gui = createUI("ScreenGui", {DisplayOrder = 999, IgnoreGuiInset = true, ResetOnSpawn = false}, CoreGui) local frame = createUI("Frame", { Size = UDim2.fromOffset(320, 145), AnchorPoint = Vector2.new(1, 1), Position = UDim2.new(1, -20, 1, -20), BackgroundColor3 = PALETTE.OuterBg, BackgroundTransparency = 1, }, gui) createUI("UICorner", {CornerRadius = UDim.new(0, 8)}, frame) createUI("UIStroke", {Color = PALETTE.OuterStroke}, frame) local promptTitle = createUI("TextLabel", { Size = UDim2.new(1, 0, 0, 35), BackgroundColor3 = PALETTE.InnerBg, Text = " Update Available (" .. tostring(newVersion) .. ")", Font = Enum.Font.GothamBold, TextColor3 = PALETTE.White, TextSize = 13, TextXAlignment = Enum.TextXAlignment.Left, }, frame) createUI("UICorner", {CornerRadius = UDim.new(0, 8)}, promptTitle) createUI("TextLabel", { Size = UDim2.new(1, -20, 0, 60), Position = UDim2.new(0.5, 0, 0, 42), AnchorPoint = Vector2.new(0.5, 0), BackgroundTransparency = 1, Font = Enum.Font.Gotham, TextColor3 = PALETTE.Muted, TextSize = 12, TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Top, TextWrapped = true, Text = changelogText or "New improvements are ready.", }, frame) local laterBtn = createUI("TextButton", { Size = UDim2.new(0.5, -12, 0, 30), Position = UDim2.new(0, 8, 1, -38), BackgroundColor3 = PALETTE.TrackBg, Font = Enum.Font.GothamBold, TextColor3 = PALETTE.White, TextSize = 12, Text = "Later", }, frame) createUI("UICorner", {CornerRadius = UDim.new(0, 6)}, laterBtn) createUI("UIStroke", {Color = PALETTE.OuterStroke, Thickness = 1}, laterBtn) local updateBtn = createUI("TextButton", { Size = UDim2.new(0.5, -12, 0, 30), Position = UDim2.new(1, -8, 1, -38), AnchorPoint = Vector2.new(1, 0), BackgroundColor3 = PALETTE.TrackHover, Font = Enum.Font.GothamBold, TextColor3 = PALETTE.Accent, TextSize = 12, Text = "Update & Reload", }, frame) createUI("UICorner", {CornerRadius = UDim.new(0, 6)}, updateBtn) createUI("UIStroke", {Color = PALETTE.Accent, Transparency = 0.35, Thickness = 1}, updateBtn) UpdatePromptGui = gui local inT = TweenInfo.new(0.4, Enum.EasingStyle.Quart, Enum.EasingDirection.Out) TweenService:Create(frame, inT, {BackgroundTransparency = 0}):Play() laterBtn.MouseButton1Click:Connect(function() saveLocalVersion(newVersion) TweenService:Create(frame, TweenInfo.new(0.3), {Position = UDim2.new(1, 350, 1, -20)}):Play() task.wait(0.3) if gui and gui.Parent then gui:Destroy() end UpdatePromptGui = nil end) updateBtn.MouseButton1Click:Connect(function() saveLocalVersion(newVersion) if UpdatePromptGui then UpdatePromptGui:Destroy() end UpdatePromptGui = nil env.ReloadScript() end) end local function HandleUpdate(latestVersion) if isAutoUpdatePermanent() then saveLocalVersion(latestVersion) env.ReloadScript() return end if UpdatePromptGui and UpdatePromptGui.Parent then UpdatePromptGui:Destroy() UpdatePromptGui = nil end -- If the picker is out, flag every script block that needs a reload -- for update (top-center icon stays until a reload-for-update succeeds). if PickerGui and PickerGui.Parent then markUpdatePush() end local c_resp = httpGet(CHANGELOG_URL) local changelogText = (c_resp and c_resp.Body) or "A new update is available." ShowUpdatePrompt(latestVersion, changelogText) end ------------------------------------------------------------------------------- -- STARTUP: show the picker, then background version check + persistent poll ------------------------------------------------------------------------------- ShowPicker() local localVer = getLocalVersion() -- Cancel any poll loop left running by a previous execution of this file, -- so re-executing never stacks duplicate updaters. if env.OspeiPollTask then pcall(task.cancel, env.OspeiPollTask) end env.OspeiPollTask = task.spawn(function() local response = httpGet(VERSION_URL) if response then local latestVersion = trim(response.Body) currentRunningVersion = latestVersion if not localVer then -- First run: just record the version; the picker decides what loads saveLocalVersion(latestVersion) elseif latestVersion ~= localVer then HandleUpdate(latestVersion) end end -- Persistent background poll — keeps catching updates across the session. while task.wait(POLL_INTERVAL) do local pollResp = httpGet(VERSION_URL) if pollResp then local liveVersion = trim(pollResp.Body) if liveVersion and liveVersion ~= currentRunningVersion then currentRunningVersion = liveVersion HandleUpdate(liveVersion) end end end end)