Steal An Exu Script (Delta Mobile) – Kryven Hub: Auto Steal, Pen Sell, Anti AFK 18 13 No Key

Steal An Exu Script (Delta Mobile) – Kryven Hub: Auto Steal, Pen Sell, Anti AFK

Steal An Exu · Roblox
Play
No key Mobile friendly Working · Nov 4
Report

Description

Kryven Hub is a Steal An Exu script you can run in Delta on Android, iOS or PC. It includes auto steal, area priority, pen full: sell and more. It shows live counters for stolen, placed, kept and sold Exus. Created by Navaaa. No key.

Key Features

  • Auto Steal: teleports to Exus, steals and returns to your pen
  • Pick areas and priority, best area first
  • Pen-full policy: Keep, Sell or Stop
  • Escape on steal
  • Teleport or Walk movement
  • WalkSpeed and JumpPower
  • Infinite Jump
  • Anti AFK
  • Teleport to pen
  • Save and load configs

Steal An Exu is a Brazilian steal-and-collect game, currently in its Exu-lloween update. You train on the treadmill to get faster, grab rarer and rarer Exus while dodging the Guardians, keep them in your pen to earn money, and fuse three identical Exus into a bigger one. Kryven Hub 3.1 by Navaaa is a Steal An Exu script that automates the stealing and gives you a status panel that shows what it’s doing.

Farm tab

Section Controls
Auto Steal Auto Steal; Return and place (on); If the pen is full: Keep in inventory, Sell or Stop; Escape on steal (on); Batch 1–5
Movement Go to Exu and Return to pen, each Teleport or Walk; Steal method Instant or Hold; Teleport settle 0–400 ms (120 by default)
Targeting Priority: Efficiency, Highest Need or Nearest; Area order: best area first, nearest first or off; Max range up to “any”
Areas A row for each area, quick-select All or Last, and “Forget spawn points” for when the map changes
Status Current state, last target, and stolen / placed / kept / sold counters

The Player tab has WalkSpeed (8–200), JumpPower, Infinite Jump and Anti AFK (on by default). Teleport has a button to go straight to your pen and one for each area.

Running it

No key, no loader. The entire hub, over 2,600 lines, is in the code box, and it doesn’t download anything. Paste it into Delta (Fluxus, Codex, Arceus X and desktop executors also made the page’s tested row) after you’ve joined. RightShift opens and closes the window, and you can change that key under Settings, along with UI scale (60–140%), background blur and pop-up notifications. Your setup is saved to a kryven_hub folder for each place and loads automatically next time unless you turn that off. “Unload hub” in Settings stops everything and removes the window.

FAQ

What happens when my pen is full? That depends on the dropdown. Keep puts new Exus in your inventory, Stop ends the run, and Sell sells them. The menu warns that selling is permanent.

Teleport or Walk? Teleport is quicker. Walk takes the normal route, which looks more natural if other players are watching. You can set each leg separately, so you could walk to the Exu and teleport home, for example.

Does it fuse Exus? No. Fusing three of the same Exu is still something you do yourself.

Pen tip: set “If the pen is full” to Keep in inventory while you’re still learning which Exus matter. A sale can’t be undone, and the one you sold might have been the third copy you needed for a fusion.

Tested With
DeltaCodexArceus XFluxusPC executors
Script CodeKryven Hub (Navaaa)
--!nocheck
-- kryven_hub.lua: Kryven Hub for Steal An Exu (auto steal core + hub UI).
--
-- Part 1 (Core): pure Lua, no Roblox API. Runs under plain Lua/Luau for tests.
-- Parts 2-4 (Roblox adapters, UI toolkit, tabs): only run when `game` exists.
--
-- Contracts consumed by the core:
--   ExuSnapshot { key:any, name:string, rarity:string?, need:number, pos:{x,y,z}, reach:number, enabled:boolean }
--   source.snapshot() -> list | nil, err        source.playerPos() -> {x,y,z} | nil
--   source.basePos()  -> {x,y,z} | nil          (needed when cfg.deposit)
--   mover.approach(pos, within, purpose) -> boolean   purpose = "target" | "base" | "escape"; true only when within `within`
--   actor.steal(snapshot, onTriggered) -> ok, item   (calls onTriggered() the moment the prompt is triggered, before
--                                                     the Tool is confirmed; ok only when acquisition was confirmed;
--                                                     item = { id = ExuId })
--   placer.place(id) -> ok, detail?             (ok only when the placement was confirmed; needed when cfg.deposit)
--   clock.now() -> seconds
--   optional: source.stillValid(snapshot) -> boolean   (false = the target no longer exists, e.g. after a reset)
--   optional: placer.holds(id) -> boolean              (false = the Exu is no longer in the player's hands)
--   optional: control.running() -> boolean             (false = the user switched the feature off; interruptions
--                                                       caused by that are not counted as failures)
--
-- Areas: the game's own zone definitions have not been read yet (UNKNOWN). Areas are INFERRED from the spawn
-- points of Runtime Exus: points closer than cfg.areaGap are one area (single linkage on x/z), areas are ordered by
-- centroid distance from cfg.areaOrigin and the farthest one is "last". Observed basis: spawn rows sit ~24-84 studs
-- apart inside a zone and >=290 studs apart between zones (z 150 / 442-450 / 802-810); pens sit within ~30 studs of
-- the world origin. Spawn points are remembered, so an area stays listed after its Exus are stolen.

local Core = {}

local function isFinite(n)
    return type(n) == "number" and n == n and n > -math.huge and n < math.huge
end

local function isVec(p)
    return type(p) == "table" and isFinite(p.x) and isFinite(p.y) and isFinite(p.z)
end

function Core.distance(a, b)
    local dx, dy, dz = a.x - b.x, a.y - b.y, a.z - b.z
    return math.sqrt(dx * dx + dy * dy + dz * dz)
end

function Core.pointKey(pos)
    return string.format("%d:%d", math.floor(pos.x + 0.5), math.floor(pos.z + 0.5))
end

function Core.defaultConfig()
    return {
        standoff = 1,                -- studs kept inside the prompt's MaxActivationDistance
        blockSeconds = 20,           -- how long a failed target is skipped
        maxConsecutiveFailures = 5,  -- stop reason "too_many_failures" after this many
        deposit = true,              -- carry stolen Exus to the own pen and place them
        batch = 1,                   -- steal this many before depositing (hand capacity is UNKNOWN)
        baseRadius = 6,              -- studs from the pen's Spawn part that count as "at base"
        returnInstant = false,       -- set by the integration when the base leg is a teleport (drops it from the estimate)
        escapeOnTrigger = false,     -- go to the base the moment the prompt is triggered, before the Tool is confirmed
        recoverSeconds = 8,          -- pause length after a transient halt before trying again
        maxHalts = 0,                -- transient halts tolerated without any success before a hard stop (0 = stop at once)
        onPlaceFail = "stop",        -- when the pen refuses an Exu: "stop" (halt) | "keep" (leave it in the inventory) | "sell"
        placeFailLimit = 2,          -- refusals in a row before placing is skipped for noPlaceSeconds
        noPlaceSeconds = 60,         -- how long placing is skipped after placeFailLimit refusals (the trip home still happens)
        areaPriority = false,        -- "best": highest area index first | "near": lowest first | false: ignore areas when ranking
        stopOnUnknownId = true,      -- stop when a stolen Exu has no id (false: leave it in the inventory and go on)
        areaGap = 120,               -- studs: spawn points closer than this belong to the same area
        areaOrigin = { x = 0, y = 0, z = 0 },
        select = {
            order = "efficiency",    -- "efficiency": Need per second of trip | "value": Need desc | "nearest"
            speed = nil,             -- studs/s, supplied per cycle by the integration; invalid -> "nearest"
            fixedSeconds = 1.5,      -- per-steal overhead in the efficiency estimate
            maxRange = math.huge,    -- studs from the player
            minNeed = -math.huge,
            maxNeed = math.huge,
            rarities = nil,          -- set { [rarity] = true } or nil for any
        },
    }
end

local function mergeConfig(user)
    local cfg = Core.defaultConfig()
    if user then
        for k, v in pairs(user) do
            if k ~= "select" then
                cfg[k] = v
            end
        end
        if user.select then
            for k, v in pairs(user.select) do
                cfg.select[k] = v
            end
        end
    end
    return cfg
end

-- Returns ok, reason. Rejects anything that would turn missing data into a believable value.
function Core.validate(s)
    if type(s) ~= "table" then
        return false, "not_a_table"
    end
    if s.key == nil then
        return false, "no_key"
    end
    if not isFinite(s.need) then
        return false, "bad_need"
    end
    if not isVec(s.pos) then
        return false, "bad_pos"
    end
    if not isFinite(s.reach) or s.reach <= 0 then
        return false, "bad_reach"
    end
    if s.enabled ~= true then
        return false, "disabled"
    end
    return true
end

-- Returns best snapshot (or nil) and stats { total, rejected = {reason=count}, filtered, orderUsed }.
-- `base` (optional vec) adds the target->base leg to the efficiency trip estimate.
-- `areaFilter` (optional) is called with the snapshot and returns whether its area is allowed.
function Core.select(snaps, me, selectCfg, isBlocked, base, areaFilter, areaRank)
    local order = selectCfg.order
    local speedOk = isFinite(selectCfg.speed) and selectCfg.speed > 0
    if order == "efficiency" and not speedOk then
        order = "nearest"
    end
    local stats = { total = #snaps, rejected = {}, filtered = 0, orderUsed = order }
    local best, bestNeed, bestDist, bestScore, bestRank
    for _, s in ipairs(snaps) do
        local ok, why = Core.validate(s)
        if not ok then
            stats.rejected[why] = (stats.rejected[why] or 0) + 1
        else
            local d = Core.distance(me, s.pos)
            local pass = d <= selectCfg.maxRange
                and s.need >= selectCfg.minNeed
                and s.need <= selectCfg.maxNeed
                and (selectCfg.rarities == nil or (s.rarity ~= nil and selectCfg.rarities[s.rarity] == true))
                and not (isBlocked and isBlocked(s.key))
                and (areaFilter == nil or areaFilter(s))
            if not pass then
                stats.filtered = stats.filtered + 1
            else
                local score
                if order == "efficiency" then
                    local trip = d + (base and Core.distance(s.pos, base) or 0)
                    local cost = math.max(trip / selectCfg.speed + (selectCfg.fixedSeconds or 0), 1e-6)
                    score = s.need / cost
                end
                local rank = areaRank and areaRank(s) or 0
                local better
                if best == nil then
                    better = true
                elseif rank ~= bestRank then
                    better = rank > bestRank -- the preferred area wins before any per-target ranking
                elseif order == "efficiency" then
                    better = score > bestScore or (score == bestScore and d < bestDist)
                elseif order == "nearest" then
                    better = d < bestDist or (d == bestDist and s.need > bestNeed)
                else
                    better = s.need > bestNeed or (s.need == bestNeed and d < bestDist)
                end
                if better then
                    best, bestNeed, bestDist, bestScore, bestRank = s, s.need, d, score, rank
                end
            end
        end
    end
    return best, stats
end

-- Single-linkage clustering of spawn points on the x/z plane.
-- points: { [key] = {x,y,z} }. Returns { list = { {index, count, centroid, dist} ... ordered by dist asc },
-- byKey = { [pointKey] = index } }.
function Core.buildAreas(points, gap, origin)
    local pts = {}
    for k, p in pairs(points) do
        pts[#pts + 1] = { x = p.x, y = p.y, z = p.z, key = k }
    end
    table.sort(pts, function(a, b)
        if a.x ~= b.x then
            return a.x < b.x
        end
        return a.z < b.z
    end)
    local parent = {}
    for i = 1, #pts do
        parent[i] = i
    end
    local function find(i)
        while parent[i] ~= i do
            parent[i] = parent[parent[i]]
            i = parent[i]
        end
        return i
    end
    for i = 1, #pts do
        for j = i + 1, #pts do
            local dx, dz = pts[i].x - pts[j].x, pts[i].z - pts[j].z
            if math.sqrt(dx * dx + dz * dz) <= gap then
                parent[find(i)] = find(j)
            end
        end
    end
    local groups, order = {}, {}
    for i = 1, #pts do
        local r = find(i)
        local g = groups[r]
        if not g then
            g = { count = 0, sx = 0, sy = 0, sz = 0, keys = {} }
            groups[r] = g
            order[#order + 1] = g
        end
        g.count = g.count + 1
        g.sx, g.sy, g.sz = g.sx + pts[i].x, g.sy + pts[i].y, g.sz + pts[i].z
        g.keys[#g.keys + 1] = pts[i].key
    end
    local list = {}
    for _, g in ipairs(order) do
        local c = { x = g.sx / g.count, y = g.sy / g.count, z = g.sz / g.count }
        local dx, dz = c.x - origin.x, c.z - origin.z
        list[#list + 1] = { count = g.count, centroid = c, dist = math.sqrt(dx * dx + dz * dz), keys = g.keys,
                            alive = 0, rarities = {} }
    end
    table.sort(list, function(a, b)
        if a.dist ~= b.dist then
            return a.dist < b.dist
        end
        return a.centroid.x < b.centroid.x
    end)
    local byKey = {}
    for i, a in ipairs(list) do
        a.index = i
        for _, k in ipairs(a.keys) do
            byKey[k] = i
        end
    end
    return { list = list, byKey = byKey }
end

-- Remembers every spawn point seen, rebuilds the areas when a new one appears and refreshes alive counts and
-- the two most common rarities per area from the current snapshot.
function Core.observe(ctl, snaps)
    local dirty = ctl.areas == nil
    for _, s in ipairs(snaps) do
        if type(s) == "table" and isVec(s.pos) then
            local k = Core.pointKey(s.pos)
            if not ctl.points[k] then
                ctl.points[k] = { x = s.pos.x, y = s.pos.y, z = s.pos.z }
                dirty = true
            end
        end
    end
    if dirty then
        ctl.areas = Core.buildAreas(ctl.points, ctl.cfg.areaGap, ctl.cfg.areaOrigin)
    end
    local counts = {}
    for _, a in ipairs(ctl.areas.list) do
        a.alive = 0
        counts[a.index] = {}
    end
    for _, s in ipairs(snaps) do
        if type(s) == "table" and isVec(s.pos) then
            local idx = ctl.areas.byKey[Core.pointKey(s.pos)]
            if idx then
                ctl.areas.list[idx].alive = ctl.areas.list[idx].alive + 1
                if type(s.rarity) == "string" then
                    counts[idx][s.rarity] = (counts[idx][s.rarity] or 0) + 1
                end
            end
        end
    end
    for _, a in ipairs(ctl.areas.list) do
        local names = {}
        for name in pairs(counts[a.index]) do
            names[#names + 1] = name
        end
        table.sort(names, function(x, y)
            local cx, cy = counts[a.index][x], counts[a.index][y]
            if cx ~= cy then
                return cx > cy
            end
            return x < y
        end)
        a.rarities = { names[1], names[2] }
    end
    return ctl.areas
end

-- Area selection: mode "all" | "last" (always the farthest known area) | "custom" (explicit index set).
function Core.selectAll(ctl)
    ctl.areaSelect = { mode = "all", set = {} }
end

function Core.selectLast(ctl)
    ctl.areaSelect = { mode = "last", set = {} }
end

function Core.isAreaSelected(ctl, idx)
    local sel = ctl.areaSelect
    if sel.mode == "all" then
        return true
    elseif sel.mode == "last" then
        return ctl.areas ~= nil and idx == #ctl.areas.list
    end
    return sel.set[idx] == true
end

function Core.toggleArea(ctl, idx)
    local sel = ctl.areaSelect
    if sel.mode == "all" then
        ctl.areaSelect = { mode = "custom", set = { [idx] = true } }
        return
    end
    if sel.mode == "last" then
        local set = {}
        if ctl.areas and #ctl.areas.list > 0 then
            set[#ctl.areas.list] = true
        end
        sel = { mode = "custom", set = set }
        ctl.areaSelect = sel
    end
    sel.set[idx] = (not sel.set[idx]) or nil
end

local function areaAllowed(ctl, s)
    if ctl.areaSelect.mode == "all" then
        return true
    end
    local idx = ctl.areas and ctl.areas.byKey[Core.pointKey(s.pos)]
    return idx ~= nil and Core.isAreaSelected(ctl, idx)
end

function Core.newController(adapters, userCfg)
    return {
        a = adapters,
        cfg = mergeConfig(userCfg),
        blocked = {},
        carried = {},   -- ExuIds stolen and not yet placed
        points = {},    -- remembered spawn points
        areas = nil,
        areaSelect = { mode = "all", set = {} },
        failures = 0,
        stolen = 0,
        placed = 0,
        lost = 0,        -- carried Exus that disappeared (reset/respawn) before they were placed
        kept = 0,        -- Exus left in the inventory because the pen refused them (or no base was found)
        sold = 0,
        placeFails = 0,
        noPlaceUntil = nil,
        haltCount = 0,   -- transient halts since the last success
        lastHalt = nil,
        pausedUntil = nil,
        stopReason = nil,
    }
end

-- Clears every stop/pause state; used when the user switches the feature back on.
function Core.reset(ctl)
    ctl.stopReason, ctl.pausedUntil, ctl.lastHalt = nil, nil, nil
    ctl.failures, ctl.haltCount = 0, 0
    ctl.placeFails, ctl.noPlaceUntil = 0, nil
end

-- Forgets the remembered spawn points (e.g. after the map changed); areas are rebuilt on the next observe.
function Core.clearAreas(ctl)
    ctl.points, ctl.areas = {}, nil
end

local function isBlocked(ctl, key)
    local untilT = ctl.blocked[key]
    if not untilT then
        return false
    end
    if ctl.a.clock.now() >= untilT then
        ctl.blocked[key] = nil
        return false
    end
    return true
end

-- Transient problems (night reset, respawn, pen not ready) pause the controller and retry; only after
-- cfg.maxHalts halts without a success does it stop for good.
local function halt(ctl, reason)
    ctl.lastHalt = reason
    ctl.haltCount = ctl.haltCount + 1
    if ctl.haltCount > ctl.cfg.maxHalts then
        ctl.stopReason = reason
    else
        ctl.pausedUntil = ctl.a.clock.now() + ctl.cfg.recoverSeconds
        ctl.failures = 0
    end
end

local function bump(ctl)
    ctl.failures = ctl.failures + 1
    if ctl.failures >= ctl.cfg.maxConsecutiveFailures then
        halt(ctl, "too_many_failures")
    end
end

local function stillValid(ctl, target)
    local check = ctl.a.source.stillValid
    return check == nil or check(target) ~= false
end

-- True when the integration says the user switched the feature off; such interruptions are not failures.
local function aborted(ctl)
    local control = ctl.a.control
    return control ~= nil and control.running() == false
end

local function fail(ctl, target, result)
    ctl.blocked[target.key] = ctl.a.clock.now() + ctl.cfg.blockSeconds
    bump(ctl)
    return result, target
end

-- Goes to the own pen and places every carried Exu. Results:
--   "deposited" {placed} | "base_unavailable" (stops) | "base_approach_failed" (retried next cycle)
--   "place_failed" {placed, detail} (stops; carried ids are kept)
function Core.deposit(ctl)
    local cfg = ctl.cfg
    local base = ctl.a.source.basePos()
    if not isVec(base) then
        if cfg.onPlaceFail == "keep" then
            local n = #ctl.carried
            ctl.kept = ctl.kept + n
            ctl.carried = {}
            return "base_unavailable", { kept = n }
        end
        halt(ctl, "base_unavailable")
        return "base_unavailable"
    end
    if not ctl.a.mover.approach(base, cfg.baseRadius, "base") then
        if aborted(ctl) then
            return "aborted"
        end
        if not isVec(ctl.a.source.playerPos()) then
            return "player_unavailable"
        end
        bump(ctl)
        return "base_approach_failed"
    end
    local placeOn = not (ctl.noPlaceUntil and ctl.a.clock.now() < ctl.noPlaceUntil)
    local placed, lostNow, keptNow, soldNow, lastDetail = 0, 0, 0, 0, nil
    while #ctl.carried > 0 do
        local id = ctl.carried[1]
        if not placeOn then
            table.remove(ctl.carried, 1)
            ctl.kept = ctl.kept + 1
            keptNow = keptNow + 1
        else
            local ok, detail = ctl.a.placer.place(id)
            if ok then
                table.remove(ctl.carried, 1)
                placed = placed + 1
                ctl.placed = ctl.placed + 1
                ctl.placeFails = 0
            elseif ctl.a.placer.holds and ctl.a.placer.holds(id) == false then
                table.remove(ctl.carried, 1) -- the Exu vanished with a reset or respawn; nothing left to place
                lostNow = lostNow + 1
                ctl.lost = ctl.lost + 1
            else
                lastDetail = detail
                ctl.placeFails = ctl.placeFails + 1
                if ctl.placeFails >= cfg.placeFailLimit then
                    ctl.noPlaceUntil = ctl.a.clock.now() + cfg.noPlaceSeconds
                    ctl.placeFails = 0
                    placeOn = false
                end
                if cfg.onPlaceFail == "stop" then
                    halt(ctl, "place_failed")
                    return "place_failed", { placed = placed, detail = detail }
                end
                local sold = false
                if cfg.onPlaceFail == "sell" and ctl.a.placer.sell then
                    sold = ctl.a.placer.sell(id) == true
                end
                table.remove(ctl.carried, 1)
                if sold then
                    ctl.sold = ctl.sold + 1
                    soldNow = soldNow + 1
                else
                    ctl.kept = ctl.kept + 1
                    keptNow = keptNow + 1
                end
            end
        end
    end
    ctl.failures = 0
    if placed > 0 or keptNow > 0 or soldNow > 0 then
        ctl.haltCount = 0
    end
    return "deposited", { placed = placed, lost = lostNow, kept = keptNow, sold = soldNow, detail = lastDetail }
end

-- One decision + action cycle. Results:
--   "stolen" | "steal_failed" | "approach_failed" (target as 2nd value) | "unknown_item_id" (stops)
--   everything returned by Core.deposit
--   "no_target" (stats) | "source_unavailable" (err) | "player_unavailable" | "stopped"
function Core.runOnce(ctl)
    if ctl.stopReason then
        return "stopped"
    end
    if ctl.pausedUntil then
        local left = ctl.pausedUntil - ctl.a.clock.now()
        if left > 0 then
            return "paused", { reason = ctl.lastHalt, remaining = left }
        end
        ctl.pausedUntil = nil
    end
    local cfg = ctl.cfg
    local now = ctl.a.clock.now()
    for key, untilT in pairs(ctl.blocked) do
        if now >= untilT then
            ctl.blocked[key] = nil
        end
    end
    if cfg.deposit and #ctl.carried >= cfg.batch then
        return Core.deposit(ctl)
    end
    local snaps, err = ctl.a.source.snapshot()
    if not snaps then
        return "source_unavailable", err
    end
    Core.observe(ctl, snaps)
    local me = ctl.a.source.playerPos()
    if not isVec(me) then
        return "player_unavailable"
    end
    local base
    if cfg.deposit and cfg.batch == 1 and not cfg.returnInstant then
        base = ctl.a.source.basePos()
        if not isVec(base) then
            base = nil
        end
    end
    local areaRank
    if cfg.areaPriority == "best" or cfg.areaPriority == "near" then
        local sign = cfg.areaPriority == "best" and 1 or -1
        areaRank = function(s)
            local idx = ctl.areas and ctl.areas.byKey[Core.pointKey(s.pos)]
            return idx and idx * sign or -math.huge
        end
    end
    local target, stats = Core.select(snaps, me, cfg.select, function(key)
        return isBlocked(ctl, key)
    end, base, function(s)
        return areaAllowed(ctl, s)
    end, areaRank)
    if not target then
        if cfg.deposit and #ctl.carried > 0 then
            return Core.deposit(ctl)
        end
        return "no_target", stats
    end
    local within = math.max(1, target.reach - cfg.standoff)
    if not ctl.a.mover.approach(target.pos, within, "target") then
        if aborted(ctl) then
            return "aborted"
        end
        if not isVec(ctl.a.source.playerPos()) then
            return "player_unavailable"
        end
        if not stillValid(ctl, target) then
            return "target_gone", target
        end
        return fail(ctl, target, "approach_failed")
    end
    local escapeBase
    if cfg.deposit and cfg.escapeOnTrigger then
        escapeBase = ctl.a.source.basePos()
        if not isVec(escapeBase) then
            escapeBase = nil
        end
    end
    local function onTriggered()
        if escapeBase then
            ctl.a.mover.approach(escapeBase, cfg.baseRadius, "escape")
        end
    end
    local ok, item = ctl.a.actor.steal(target, onTriggered)
    if not ok then
        if aborted(ctl) then
            return "aborted"
        end
        if not stillValid(ctl, target) then
            return "target_gone", target
        end
        return fail(ctl, target, "steal_failed")
    end
    ctl.stolen = ctl.stolen + 1
    ctl.failures = 0
    ctl.haltCount = 0
    if cfg.deposit then
        if type(item) ~= "table" or item.id == nil then
            if cfg.stopOnUnknownId then
                ctl.stopReason = "unknown_item_id"
                return "unknown_item_id", target
            end
            return "stolen", target -- cannot be placed without an id; it stays in the inventory
        end
        ctl.carried[#ctl.carried + 1] = item.id
        if #ctl.carried >= cfg.batch then
            return Core.deposit(ctl)
        end
    end
    return "stolen", target
end

if game == nil then
    return Core
end

-- ===================================================================================================
-- Part 2: Roblox services, shared state and target adapters
-- ===================================================================================================

local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")
local HttpService = game:GetService("HttpService")
local Lighting = game:GetService("Lighting")
local Stats = game:GetService("Stats")
local MarketplaceService = game:GetService("MarketplaceService")
local lp = Players.LocalPlayer

local VERSION = "3.1"
local HUB_NAME = "KryvenHub"
local env = (type(getgenv) == "function" and getgenv()) or _G

-- a second execution replaces the first instance
if type(env.__KryvenUnload) == "function" then
    pcall(env.__KryvenUnload)
end

local State = { alive = true, enabled = false }
local connections = {}

local function bind(signal, fn)
    local c = signal:Connect(fn)
    connections[#connections + 1] = c
    return c
end

local function safe(fn, ...)
    local ok, err = pcall(fn, ...)
    if not ok then
        warn("[kryven] " .. tostring(err))
    end
    return ok
end

local CFG = {
    moveTarget = "teleport", -- how to reach an Exu: "teleport" (CFrame) | "walk" (Humanoid:MoveTo)
    moveBase = "teleport",   -- how to return to the pen after a steal: "teleport" | "walk"
    settleSeconds = 0.12,    -- pause after a teleport so the server sees the new position before the next action
    hold = "fire",           -- "fire" = fireproximityprompt, falls back to a real hold | "hold" = real hold only
    approachTimeout = 20,    -- seconds to reach a point before it counts as a failure
    holdPadding = 0.05,      -- extra seconds on top of the prompt's HoldDuration
    fireConfirm = 0.8,       -- seconds to see the Tool after fireproximityprompt before falling back to a hold
    confirmTimeout = 3,      -- seconds to confirm a real hold / a placement
    invokeTimeout = 5,       -- seconds to wait for the server to answer a placement call
    loopDelay = 0,
    idleDelay = 1,
    teleportSpeed = 500,     -- speed fed to the efficiency estimate while teleporting to Exus
    core = {
        standoff = 1,
        blockSeconds = 20,
        maxConsecutiveFailures = 5,
        deposit = true,
        batch = 1,
        baseRadius = 6,
        areaGap = 120,
        escapeOnTrigger = true,
        recoverSeconds = 5,         -- pause after a transient halt (night reset, respawn) before trying again
        maxHalts = math.huge,       -- never switch off on its own; only the user's toggle stops the farm
        onPlaceFail = "keep",       -- a refused placement (pen full) must not stop the farm
        areaPriority = "best",      -- best selected area first, then the next one when it has no Exus left
        stopOnUnknownId = false,
        select = { order = "efficiency", maxRange = math.huge, fixedSeconds = 1.5 },
    },
}

local fireUnreliable = false -- set after two instant triggers in a row produced no Tool; real holds are used afterwards
local fireMisses = 0
local timeline = { tTrigger = 0, tTool = 0, tTp = 0, tPlaced = 0 } -- os.clock() stamps of the current steal, 0 = not reached

local function toVec(v)
    return { x = v.X, y = v.Y, z = v.Z }
end

local function character()
    local char = lp.Character
    local hum = char and char:FindFirstChildOfClass("Humanoid")
    local hrp = char and char:FindFirstChild("HumanoidRootPart")
    if hum and hrp and hrp:IsA("BasePart") and hum.Health > 0 then
        return char, hum, hrp
    end
    return nil
end

local function promptOf(model)
    local root = model:FindFirstChild("Root")
    if not (root and root:IsA("BasePart")) then
        return nil, nil
    end
    local prompt = root:FindFirstChildOfClass("ProximityPrompt")
    if prompt and prompt.ActionText == "Roubar" then
        return prompt, root
    end
    return nil, nil
end

local function running()
    return State.alive and State.enabled
end

-- ExuIds of every Tool the player currently holds (Backpack + Character)
local function toolIds()
    local set = {}
    local function scan(container)
        if container then
            for _, t in ipairs(container:GetChildren()) do
                if t:IsA("Tool") then
                    local id = t:GetAttribute("ExuId")
                    if id ~= nil then
                        set[id] = true
                    end
                end
            end
        end
    end
    scan(lp:FindFirstChild("Backpack"))
    scan(lp.Character)
    return set
end

local Source = {}

function Source.snapshot()
    local runtime = Workspace:FindFirstChild("Runtime")
    if not runtime then
        return nil, "Workspace.Runtime missing"
    end
    local out = {}
    for _, m in ipairs(runtime:GetChildren()) do
        if m:IsA("Model") then
            local prompt, root = promptOf(m)
            local need = m:GetAttribute("Need")
            if prompt and typeof(need) == "number" then
                out[#out + 1] = {
                    key = m,
                    name = prompt.ObjectText,
                    rarity = m:GetAttribute("Rarity"),
                    need = need,
                    pos = toVec(root.Position),
                    reach = prompt.MaxActivationDistance,
                    enabled = prompt.Enabled,
                }
            end
        end
    end
    return out
end

function Source.playerPos()
    local _, _, hrp = character()
    return hrp and toVec(hrp.Position) or nil
end

-- A reset or another player's steal removes the model or disables its prompt.
function Source.stillValid(snap)
    local model = snap.key
    if not (model and model.Parent) then
        return false
    end
    local prompt = promptOf(model)
    return prompt ~= nil and prompt.Enabled
end

-- Own pen: Owner attribute == UserId first, then the player's PenName attribute.
local function ownPen()
    local map = Workspace:FindFirstChild("Map")
    local pens = map and map:FindFirstChild("Pens")
    if not pens then
        return nil
    end
    for _, p in ipairs(pens:GetChildren()) do
        if p:GetAttribute("Owner") == lp.UserId then
            return p
        end
    end
    local name = lp:GetAttribute("PenName")
    if typeof(name) == "string" then
        return pens:FindFirstChild(name)
    end
    return nil
end

function Source.basePos()
    local pen = ownPen()
    local spawn = pen and pen:FindFirstChild("Spawn")
    if spawn and spawn:IsA("BasePart") then
        return toVec(spawn.Position)
    end
    return nil
end

local Mover = {}

function Mover.approach(pos, within, purpose)
    local mode
    if purpose == "escape" then
        mode = "teleport" -- the whole point of escaping is speed
    elseif purpose == "base" then
        mode = CFG.moveBase
    else
        mode = CFG.moveTarget
    end
    local target = Vector3.new(pos.x, pos.y, pos.z)
    local deadline = os.clock() + CFG.approachTimeout
    local lastMove = 0
    while running() and os.clock() < deadline do
        local _, hum, hrp = character()
        if not hrp then
            return false
        end
        if (hrp.Position - target).Magnitude <= within then
            return true
        end
        if mode == "teleport" then
            if purpose ~= "target" and timeline.tTp == 0 then
                timeline.tTp = os.clock()
            end
            -- offset magnitude is at most 0.67*within, so the next distance check passes
            hrp.CFrame = CFrame.new(target + Vector3.new(0, math.min(3, within * 0.6), within * 0.3))
            hrp.AssemblyLinearVelocity = Vector3.zero
            task.wait(CFG.settleSeconds)
        elseif os.clock() - lastMove > 0.25 then
            hum:MoveTo(target)
            lastMove = os.clock()
            task.wait()
        else
            task.wait()
        end
    end
    return false
end

local Actor = {}

function Actor.steal(snap, onTriggered)
    local model = snap.key
    local prompt = model.Parent and promptOf(model)
    if not (prompt and prompt.Enabled) then
        return false
    end
    timeline.tTrigger, timeline.tTool, timeline.tTp, timeline.tPlaced = 0, 0, 0, 0
    local known = toolIds()
    local newId
    local conns = {}
    local function onChild(child)
        if child:IsA("Tool") then
            local id = child:GetAttribute("ExuId")
            if id ~= nil and not known[id] then
                newId = id
                timeline.tTool = os.clock()
            end
        end
    end
    local backpack = lp:FindFirstChild("Backpack")
    if backpack then
        conns[#conns + 1] = backpack.ChildAdded:Connect(onChild)
    end
    if lp.Character then
        conns[#conns + 1] = lp.Character.ChildAdded:Connect(onChild)
    end

    local useFire = CFG.hold == "fire" and type(fireproximityprompt) == "function" and not fireUnreliable
    local modes = useFire and { "fire", "hold" } or { "hold" }
    local escaped = false
    for _, mode in ipairs(modes) do
        if mode == "fire" then
            pcall(fireproximityprompt, prompt)
        else
            pcall(function() prompt:InputHoldBegin() end)
            task.wait(prompt.HoldDuration + CFG.holdPadding)
            pcall(function() prompt:InputHoldEnd() end)
        end
        timeline.tTrigger = os.clock()
        if onTriggered and not escaped then
            escaped = true
            onTriggered() -- leave before the server answers; Tool detection keeps running on its own connection
        end
        local deadline = os.clock() + (mode == "fire" and CFG.fireConfirm or CFG.confirmTimeout)
        while newId == nil and os.clock() < deadline and running() do
            task.wait()
        end
        if newId ~= nil or escaped or not (model.Parent and prompt.Parent) then
            if newId == nil and escaped and mode == "fire" and model.Parent and prompt.Parent then
                fireMisses = fireMisses + 1 -- target still there but no Tool: count it against the instant trigger
                if fireMisses >= 2 then
                    fireUnreliable = true
                end
            end
            break
        end
    end
    for _, c in ipairs(conns) do
        c:Disconnect()
    end
    if newId ~= nil then
        fireMisses = 0
        return true, { id = newId }
    end
    return false
end

local Remotes = ReplicatedStorage:FindFirstChild("Remotes")
local Placer = {}

-- Runs Shop:InvokeServer(action, id) and reports whether the inventory really changed.
local function inventoryCall(action, id)
    local shop = Remotes and Remotes:FindFirstChild("Shop")
    if not (shop and shop:IsA("RemoteFunction")) then
        return false, "Remotes.Shop missing"
    end
    local hadTool = toolIds()[id] == true
    local invChanged = false
    local conn = lp:GetAttributeChangedSignal("Inv"):Connect(function()
        invChanged = true
    end)
    -- InvokeServer yields until the server answers; run it on its own thread so a silent server cannot freeze the loop
    local done, ok, r1, r2 = false, nil, nil, nil
    task.spawn(function()
        ok, r1, r2 = pcall(function()
            return shop:InvokeServer(action, id)
        end)
        done = true
    end)
    local invokeDeadline = os.clock() + CFG.invokeTimeout
    while not done and os.clock() < invokeDeadline and State.alive do
        task.wait()
    end
    local function confirmed()
        return invChanged or (hadTool and toolIds()[id] ~= true)
    end
    local deadline = os.clock() + CFG.confirmTimeout
    while not confirmed() and os.clock() < deadline and State.alive do
        task.wait()
    end
    conn:Disconnect()
    local changed = confirmed()
    if changed and action == "placeInv" then
        timeline.tPlaced = os.clock()
    end
    return changed, string.format("%s done=%s ok=%s r1=%s r2=%s", action, tostring(done), tostring(ok), tostring(r1), tostring(r2))
end

function Placer.place(id)
    return inventoryCall("placeInv", id)
end

-- permanent: only reached when the user chose "Sell" for a full pen
function Placer.sell(id)
    return inventoryCall("sellInv", id)
end

function Placer.holds(id)
    return toolIds()[id] == true
end

local ctl = Core.newController({
    source = Source,
    mover = Mover,
    actor = Actor,
    placer = Placer,
    control = { running = running },
    clock = { now = os.clock },
}, CFG.core)

-- ===================================================================================================
-- Part 3: Kryven UI toolkit
-- ===================================================================================================

-- Enum.KeyCode[name] throws on an unknown name; this returns nil instead
local function keyCode(name)
    local ok, value = pcall(function()
        return Enum.KeyCode[name]
    end)
    return ok and value or nil
end

local T = {
    bg = Color3.fromRGB(13, 15, 15),
    panel = Color3.fromRGB(16, 19, 19),
    card = Color3.fromRGB(19, 23, 22),
    hover = Color3.fromRGB(27, 33, 31),
    line = Color3.fromRGB(33, 39, 38),
    text = Color3.fromRGB(236, 240, 238),
    dim = Color3.fromRGB(121, 133, 129),
    accent = Color3.fromRGB(34, 197, 94),
    accentDim = Color3.fromRGB(18, 44, 32),
    accentText = Color3.fromRGB(10, 22, 14),
    off = Color3.fromRGB(46, 52, 51),
    offKnob = Color3.fromRGB(112, 122, 119),
    danger = Color3.fromRGB(239, 68, 68),
}

local W, H = 780, 500
local TOP, STATUS, SIDE = 52, 30, 176

local function new(class, props, parent)
    local inst = Instance.new(class)
    for k, v in pairs(props or {}) do
        inst[k] = v
    end
    if parent then
        inst.Parent = parent
    end
    return inst
end

local function corner(inst, radius)
    return new("UICorner", { CornerRadius = UDim.new(0, radius) }, inst)
end

local function stroke(inst, color, thickness)
    return new("UIStroke", {
        Color = color, Thickness = thickness or 1, ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
    }, inst)
end

local function listLayout(inst, direction, padding)
    return new("UIListLayout", {
        FillDirection = direction or Enum.FillDirection.Vertical,
        Padding = UDim.new(0, padding or 0),
        SortOrder = Enum.SortOrder.LayoutOrder,
    }, inst)
end

local function makeText(props, parent)
    local p = {
        BackgroundTransparency = 1,
        Font = Enum.Font.Gotham,
        TextColor3 = T.text,
        TextSize = 13,
        TextXAlignment = Enum.TextXAlignment.Left,
        TextTruncate = Enum.TextTruncate.AtEnd,
    }
    for k, v in pairs(props) do
        p[k] = v
    end
    return new("TextLabel", p, parent)
end

local function tween(inst, seconds, props, style)
    local t = TweenService:Create(inst, TweenInfo.new(seconds, style or Enum.EasingStyle.Quad, Enum.EasingDirection.Out), props)
    t:Play()
    return t
end

local gui = new("ScreenGui", {
    Name = HUB_NAME,
    ResetOnSpawn = false,
    DisplayOrder = 999,
    IgnoreGuiInset = true,
    ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
})

local function mountGui()
    local candidates = {}
    if type(gethui) == "function" then
        local ok, h = pcall(gethui)
        if ok and h then
            candidates[#candidates + 1] = h
        end
    end
    local okCore, core = pcall(game.GetService, game, "CoreGui")
    if okCore then
        candidates[#candidates + 1] = core
    end
    candidates[#candidates + 1] = lp:WaitForChild("PlayerGui")
    for _, parent in ipairs(candidates) do
        for _, name in ipairs({ HUB_NAME, "AutoStealV3HUD", "AutoStealV2HUD" }) do
            local old = parent:FindFirstChild(name)
            if old then
                old:Destroy()
            end
        end
        if pcall(function() gui.Parent = parent end) and gui.Parent == parent then
            return true
        end
    end
    return false
end

-- Drag `target` by `handle`; a press that barely moves counts as a click.
local function draggable(handle, target, onClick)
    local dragging, moved, startMouse, startPos = false, false, nil, nil
    bind(handle.InputBegan, function(input)
        if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
            dragging, moved = true, false
            startMouse, startPos = input.Position, target.Position
        end
    end)
    bind(UserInputService.InputChanged, function(input)
        if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then
            local delta = input.Position - startMouse
            if delta.Magnitude > 4 then
                moved = true
            end
            if moved then
                target.Position = UDim2.new(startPos.X.Scale, startPos.X.Offset + delta.X,
                    startPos.Y.Scale, startPos.Y.Offset + delta.Y)
            end
        end
    end)
    bind(UserInputService.InputEnded, function(input)
        if dragging and (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch) then
            dragging = false
            if not moved and onClick then
                safe(onClick)
            end
        end
    end)
end

local function makeIcon(kind, parent)
    local box = new("Frame", {
        Size = UDim2.fromOffset(14, 14), Position = UDim2.new(0, 14, 0.5, -7), BackgroundTransparency = 1,
    }, parent)
    local fills, strokes = {}, {}
    if kind == "diamond" then
        fills[1] = new("Frame", {
            Size = UDim2.fromOffset(8, 8), AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5),
            Rotation = 45, BorderSizePixel = 0, BackgroundColor3 = T.dim,
        }, box)
    elseif kind == "dot" then
        fills[1] = new("Frame", {
            Size = UDim2.fromOffset(8, 8), AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5),
            BorderSizePixel = 0, BackgroundColor3 = T.dim,
        }, box)
        corner(fills[1], 4)
    elseif kind == "ring" then
        local ring = new("Frame", {
            Size = UDim2.fromOffset(10, 10), AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5),
            BorderSizePixel = 0, BackgroundTransparency = 1,
        }, box)
        corner(ring, 5)
        strokes[1] = stroke(ring, T.dim, 2)
    else
        for i = 0, 2 do
            fills[#fills + 1] = new("Frame", {
                Size = UDim2.fromOffset(10, 2), Position = UDim2.fromOffset(2, 2 + i * 4),
                BorderSizePixel = 0, BackgroundColor3 = T.dim,
            }, box)
        end
    end
    return {
        setColor = function(c)
            for _, f in ipairs(fills) do
                f.BackgroundColor3 = c
            end
            for _, s in ipairs(strokes) do
                s.Color = c
            end
        end,
    }
end

local Hub = {
    tabs = {}, current = nil, flags = {}, controls = {},
    ready = false, hidden = false, minimized = false,
    notifications = true, blurWanted = false, toggleKey = Enum.KeyCode.RightShift, listening = false,
}
local Tab, Section = {}, {}
Tab.__index = Tab
Section.__index = Section

-- toasts ----------------------------------------------------------------------------------------------
local toastOrder = 0
function Hub:Notify(title, message, force)
    if not (self.toastHolder and (self.notifications or force)) then
        return
    end
    toastOrder = toastOrder + 1
    local toast = new("Frame", {
        Size = UDim2.fromOffset(250, 0), BackgroundColor3 = T.card, BorderSizePixel = 0,
        ClipsDescendants = true, LayoutOrder = toastOrder,
    }, self.toastHolder)
    corner(toast, 8)
    stroke(toast, T.line, 1)
    new("Frame", { Size = UDim2.new(0, 3, 1, 0), BackgroundColor3 = T.accent, BorderSizePixel = 0 }, toast)
    makeText({ Position = UDim2.fromOffset(14, 7), Size = UDim2.new(1, -24, 0, 16), Text = title,
        Font = Enum.Font.GothamBold, TextSize = 12 }, toast)
    makeText({ Position = UDim2.fromOffset(14, 25), Size = UDim2.new(1, -24, 0, 16), Text = message,
        TextSize = 11, TextColor3 = T.dim }, toast)
    tween(toast, 0.2, { Size = UDim2.fromOffset(250, 50) })
    task.delay(2.6, function()
        if toast.Parent then
            tween(toast, 0.2, { Size = UDim2.fromOffset(250, 0) })
            task.wait(0.25)
            toast:Destroy()
        end
    end)
end

-- dropdown popup --------------------------------------------------------------------------------------
function Hub:_closePopup()
    if self.popup then
        self.popup.overlay:Destroy()
        self.popup.frame:Destroy()
        self.popup = nil
    end
end

function Hub:_openPopup(anchor, options, current, onPick)
    self:_closePopup()
    local s = self.window.AbsoluteSize.X / W
    local rel = (anchor.AbsolutePosition - self.window.AbsolutePosition) / s
    local width, anchorH = anchor.AbsoluteSize.X / s, anchor.AbsoluteSize.Y / s
    local height = #options * 28 + 8
    local y = rel.Y + anchorH + 4
    if y + height > H - STATUS then
        y = rel.Y - height - 4
    end
    local overlay = new("TextButton", {
        Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, Text = "", AutoButtonColor = false, ZIndex = 40,
    }, self.window)
    local frame = new("Frame", {
        Size = UDim2.fromOffset(width, height), Position = UDim2.fromOffset(rel.X, y),
        BackgroundColor3 = T.card, BorderSizePixel = 0, ZIndex = 41,
    }, self.window)
    corner(frame, 8)
    stroke(frame, T.line, 1)
    self.popup = { overlay = overlay, frame = frame }
    overlay.MouseButton1Click:Connect(function() self:_closePopup() end)
    for i, option in ipairs(options) do
        local selected = option == current
        local b = new("TextButton", {
            Size = UDim2.new(1, -8, 0, 26), Position = UDim2.fromOffset(4, 4 + (i - 1) * 28),
            BackgroundColor3 = T.accentDim, BackgroundTransparency = selected and 0 or 1, BorderSizePixel = 0,
            Text = option, TextColor3 = selected and T.accent or T.text, Font = Enum.Font.Gotham, TextSize = 12,
            AutoButtonColor = false, ZIndex = 42,
        }, frame)
        corner(b, 6)
        b.MouseEnter:Connect(function()
            if not selected then
                tween(b, 0.1, { BackgroundTransparency = 0, BackgroundColor3 = T.hover })
            end
        end)
        b.MouseLeave:Connect(function()
            if not selected then
                tween(b, 0.1, { BackgroundTransparency = 1 })
            end
        end)
        b.MouseButton1Click:Connect(function()
            self:_closePopup()
            onPick(option)
        end)
    end
end

-- sections and controls -------------------------------------------------------------------------------
function Section:_refresh()
    local total, on = 0, 0
    for _, c in ipairs(self.toggles) do
        total = total + 1
        if c.value then
            on = on + 1
        end
    end
    self.countLabel.Text = total > 0 and (on .. "/" .. total .. " on") or ""
    Hub:_updateHeader()
end

function Section:_row(name, desc, height)
    local row = new("Frame", {
        Size = UDim2.new(1, 0, 0, height), BackgroundTransparency = 1, LayoutOrder = #self.rows + 1,
    }, self.body)
    new("Frame", { Size = UDim2.new(1, 0, 0, 1), BackgroundColor3 = T.line, BorderSizePixel = 0 }, row)
    if name then
        if desc then
            makeText({ Position = UDim2.fromOffset(16, 8), Size = UDim2.new(1, -150, 0, 18), Text = name,
                Font = Enum.Font.GothamMedium, TextSize = 13 }, row)
            makeText({ Position = UDim2.fromOffset(16, 27), Size = UDim2.new(1, -150, 0, 14), Text = desc,
                TextSize = 11, TextColor3 = T.dim }, row)
        else
            makeText({ Position = UDim2.fromOffset(16, 0), Size = UDim2.new(1, -150, 1, 0), Text = name,
                Font = Enum.Font.GothamMedium, TextSize = 13 }, row)
        end
    end
    self.rows[#self.rows + 1] = { frame = row, key = ((name or "") .. " " .. (desc or "")):lower() }
    return row
end

function Section:_register(ctrl, o, isFeature)
    ctrl.default = ctrl.value
    ctrl.noReset = o.noReset == true
    self.tab.controls[#self.tab.controls + 1] = ctrl
    if isFeature then
        self.tab.featureCount = self.tab.featureCount + 1
        self.tab.countLabel.Text = tostring(self.tab.featureCount)
    end
    if o.flag then
        Hub.controls[o.flag] = ctrl
        if not o.noSave then
            Hub.flags[o.flag] = ctrl.value
        end
    end
end

local function emit(o, ctrl, flagSaved)
    if o.flag and flagSaved then
        Hub.flags[o.flag] = ctrl.value
    end
    if o.callback then
        safe(o.callback, ctrl.value)
    end
end

function Section:Toggle(o)
    local row = self:_row(o.name, o.desc, o.desc and 52 or 44)
    local track = new("TextButton", {
        Size = UDim2.fromOffset(40, 22), AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -16, 0.5, 0),
        BackgroundColor3 = T.off, BorderSizePixel = 0, Text = "", AutoButtonColor = false,
    }, row)
    corner(track, 11)
    local knob = new("Frame", {
        Size = UDim2.fromOffset(16, 16), Position = UDim2.fromOffset(3, 3), BackgroundColor3 = T.offKnob, BorderSizePixel = 0,
    }, track)
    corner(knob, 8)
    local ctrl = { kind = "toggle", value = false }
    local section = self
    local function visual(v)
        tween(track, 0.15, { BackgroundColor3 = v and T.accent or T.off })
        tween(knob, 0.15, {
            Position = v and UDim2.fromOffset(21, 3) or UDim2.fromOffset(3, 3),
            BackgroundColor3 = v and T.accentText or T.offKnob,
        })
    end
    function ctrl.Set(v, silent)
        v = v == true
        if v == ctrl.value and not silent then
            return
        end
        ctrl.value = v
        visual(v)
        section:_refresh()
        if not silent then
            emit(o, ctrl, not o.noSave)
            if Hub.ready and o.notify ~= false then
                Hub:Notify(o.name, v and "Enabled" or "Disabled")
            end
        end
    end
    track.MouseButton1Click:Connect(function()
        ctrl.Set(not ctrl.value)
    end)
    ctrl.value = o.default == true
    visual(ctrl.value)
    self.toggles[#self.toggles + 1] = ctrl
    self:_register(ctrl, o, true)
    self:_refresh()
    if o.callback then
        safe(o.callback, ctrl.value)
    end
    return ctrl
end

function Section:Slider(o)
    local row = self:_row(o.name, nil, 58)
    local step = o.step or 1
    local fmt = o.format or function(v) return tostring(v) end
    local valueLabel = makeText({
        AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -16, 0, 8), Size = UDim2.fromOffset(130, 18),
        TextXAlignment = Enum.TextXAlignment.Right, RichText = true, Font = Enum.Font.RobotoMono, TextSize = 12,
    }, row)
    local track = new("Frame", {
        Position = UDim2.fromOffset(16, 40), Size = UDim2.new(1, -32, 0, 4), BackgroundColor3 = T.off, BorderSizePixel = 0,
    }, row)
    corner(track, 2)
    local fill = new("Frame", { Size = UDim2.fromScale(0, 1), BackgroundColor3 = T.accent, BorderSizePixel = 0 }, track)
    corner(fill, 2)
    local knob = new("Frame", {
        Size = UDim2.fromOffset(14, 14), AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0, 0.5),
        BackgroundColor3 = T.text, BorderSizePixel = 0, ZIndex = 2,
    }, track)
    corner(knob, 7)
    local hit = new("TextButton", {
        Position = UDim2.fromOffset(16, 30), Size = UDim2.new(1, -32, 0, 24), BackgroundTransparency = 1, Text = "",
        AutoButtonColor = false,
    }, row)
    local ctrl = { kind = "slider", value = o.min }
    local function render(v)
        local frac = (v - o.min) / math.max(o.max - o.min, 1e-9)
        fill.Size = UDim2.fromScale(frac, 1)
        knob.Position = UDim2.fromScale(frac, 0.5)
        valueLabel.Text = string.format('<b>%s</b> <font color="rgb(121,133,129)">/ %s</font>', fmt(v), fmt(o.max))
    end
    function ctrl.Set(v, silent)
        v = o.min + math.floor((v - o.min) / step + 0.5) * step
        v = math.clamp(v, o.min, o.max)
        if v == ctrl.value and not silent then
            return
        end
        ctrl.value = v
        render(v)
        if not silent then
            emit(o, ctrl, not o.noSave)
        end
    end
    local dragging = false
    local function fromInput(input)
        local rel = math.clamp((input.Position.X - track.AbsolutePosition.X) / math.max(track.AbsoluteSize.X, 1), 0, 1)
        ctrl.Set(o.min + rel * (o.max - o.min))
    end
    hit.InputBegan:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
            dragging = true
            fromInput(input)
        end
    end)
    bind(UserInputService.InputChanged, function(input)
        if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then
            fromInput(input)
        end
    end)
    bind(UserInputService.InputEnded, function(input)
        if dragging and (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch) then
            dragging = false
        end
    end)
    ctrl.value = math.clamp(o.default or o.min, o.min, o.max)
    render(ctrl.value)
    self:_register(ctrl, o, true)
    if o.callback then
        safe(o.callback, ctrl.value)
    end
    return ctrl
end

function Section:Dropdown(o)
    local row = self:_row(o.name, o.desc, o.desc and 52 or 44)
    local btn = new("TextButton", {
        Size = UDim2.fromOffset(136, 28), AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -16, 0.5, 0),
        BackgroundColor3 = T.panel, BorderSizePixel = 0, Text = "", AutoButtonColor = false,
    }, row)
    corner(btn, 6)
    stroke(btn, T.line, 1)
    local txt = makeText({ Position = UDim2.fromOffset(10, 0), Size = UDim2.new(1, -28, 1, 0), TextSize = 12 }, btn)
    makeText({ Position = UDim2.new(1, -20, 0, 0), Size = UDim2.fromOffset(16, 28), Text = "v", Font = Enum.Font.RobotoMono,
        TextSize = 11, TextColor3 = T.dim, TextXAlignment = Enum.TextXAlignment.Center }, btn)
    local ctrl = { kind = "dropdown", value = o.options[1] }
    function ctrl.Set(v, silent)
        local known = false
        for _, option in ipairs(o.options) do
            if option == v then
                known = true
            end
        end
        if not known or (v == ctrl.value and not silent) then
            return
        end
        ctrl.value = v
        txt.Text = v
        if not silent then
            emit(o, ctrl, not o.noSave)
        end
    end
    btn.MouseButton1Click:Connect(function()
        Hub:_openPopup(btn, o.options, ctrl.value, function(option)
            ctrl.Set(option)
        end)
    end)
    btn.MouseEnter:Connect(function() tween(btn, 0.1, { BackgroundColor3 = T.hover }) end)
    btn.MouseLeave:Connect(function() tween(btn, 0.1, { BackgroundColor3 = T.panel }) end)
    ctrl.value = o.default or o.options[1]
    txt.Text = ctrl.value
    self:_register(ctrl, o, true)
    if o.callback then
        safe(o.callback, ctrl.value)
    end
    return ctrl
end

function Section:Keybind(o)
    local row = self:_row(o.name, o.desc, o.desc and 52 or 44)
    local btn = new("TextButton", {
        Size = UDim2.fromOffset(110, 28), AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -16, 0.5, 0),
        BackgroundColor3 = T.panel, BorderSizePixel = 0, AutoButtonColor = false, Text = "",
        Font = Enum.Font.RobotoMono, TextSize = 12, TextColor3 = T.text,
    }, row)
    corner(btn, 6)
    stroke(btn, T.line, 1)
    local ctrl = { kind = "keybind", value = o.default or "RightShift" }
    function ctrl.Set(v, silent)
        if keyCode(v) == nil then
            return
        end
        ctrl.value = v
        btn.Text = v
        if not silent then
            emit(o, ctrl, not o.noSave)
        end
    end
    btn.MouseButton1Click:Connect(function()
        Hub.listening = true
        btn.Text = "press a key..."
        local conn
        conn = UserInputService.InputBegan:Connect(function(input)
            if input.UserInputType == Enum.UserInputType.Keyboard then
                conn:Disconnect()
                Hub.listening = false
                if input.KeyCode ~= Enum.KeyCode.Escape then
                    ctrl.Set(input.KeyCode.Name)
                else
                    btn.Text = ctrl.value
                end
            end
        end)
    end)
    btn.Text = ctrl.value
    self:_register(ctrl, o, true)
    if o.callback then
        safe(o.callback, ctrl.value)
    end
    return ctrl
end

-- o.buttons = { { text = "...", callback = fn, accent = bool }, ... }
function Section:Buttons(o)
    local row = self:_row(o.name, o.desc, o.desc and 52 or 44)
    local x = 16
    for i = #o.buttons, 1, -1 do
        local spec = o.buttons[i]
        local width = spec.width or 84
        local b = new("TextButton", {
            Size = UDim2.fromOffset(width, 28), AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -x, 0.5, 0),
            BackgroundColor3 = spec.accent and T.accentDim or T.panel, BorderSizePixel = 0, AutoButtonColor = false,
            Text = spec.text, TextColor3 = spec.accent and T.accent or (spec.danger and T.danger or T.text),
            Font = Enum.Font.GothamMedium, TextSize = 12,
        }, row)
        corner(b, 6)
        stroke(b, spec.accent and T.accent or T.line, 1)
        local base = spec.accent and T.accentDim or T.panel
        b.MouseEnter:Connect(function() tween(b, 0.1, { BackgroundColor3 = T.hover }) end)
        b.MouseLeave:Connect(function() tween(b, 0.1, { BackgroundColor3 = base }) end)
        b.MouseButton1Click:Connect(function()
            if spec.callback then
                safe(spec.callback)
            end
        end)
        x = x + width + 8
    end
    return row
end

function Section:Info(o)
    local row = self:_row(o.name, nil, 38)
    local value = makeText({
        AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -16, 0, 0), Size = UDim2.new(0.55, -16, 1, 0),
        Text = o.value or "", TextXAlignment = Enum.TextXAlignment.Right, Font = Enum.Font.RobotoMono, TextSize = 11,
        TextColor3 = T.dim,
    }, row)
    return { Set = function(text) value.Text = text end }
end

-- an empty, auto-sized frame for dynamic content (area lists)
function Section:Container(name)
    local row = self:_row(nil, nil, 0)
    row.Size = UDim2.new(1, 0, 0, 0)
    row.AutomaticSize = Enum.AutomaticSize.Y
    self.rows[#self.rows].key = (name or ""):lower()
    local box = new("Frame", {
        Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1,
    }, row)
    new("UIPadding", { PaddingTop = UDim.new(0, 6), PaddingBottom = UDim.new(0, 8), PaddingLeft = UDim.new(0, 8),
        PaddingRight = UDim.new(0, 8) }, box)
    listLayout(box, Enum.FillDirection.Vertical, 4)
    return box
end

function Tab:Section(title, col)
    col = col or ((#self.sections % 2) + 1)
    local card = new("Frame", {
        Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundColor3 = T.card, BorderSizePixel = 0,
        LayoutOrder = #self.sections + 1,
    }, self.colFrames[col])
    corner(card, 10)
    stroke(card, T.line, 1)
    listLayout(card, Enum.FillDirection.Vertical, 0)
    local header = new("Frame", { Size = UDim2.new(1, 0, 0, 36), BackgroundTransparency = 1, LayoutOrder = 0 }, card)
    makeText({ Position = UDim2.fromOffset(16, 0), Size = UDim2.new(0.6, 0, 1, 0), Text = title,
        Font = Enum.Font.RobotoMono, TextSize = 11, TextColor3 = T.dim }, header)
    local countLabel = makeText({
        AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -16, 0, 0), Size = UDim2.new(0.4, 0, 1, 0), Text = "",
        TextXAlignment = Enum.TextXAlignment.Right, Font = Enum.Font.RobotoMono, TextSize = 11, TextColor3 = T.dim,
    }, header)
    local body = new("Frame", {
        Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = 1,
    }, card)
    listLayout(body, Enum.FillDirection.Vertical, 0)
    local sec = setmetatable({
        tab = self, card = card, body = body, countLabel = countLabel, rows = {}, toggles = {}, titleKey = title:lower(),
    }, Section)
    self.sections[#self.sections + 1] = sec
    return sec
end

function Tab:ActiveCount()
    local n = 0
    for _, sec in ipairs(self.sections) do
        for _, c in ipairs(sec.toggles) do
            if c.value then
                n = n + 1
            end
        end
    end
    return n
end

function Hub:_updateHeader()
    local tab = self.current
    if not (tab and self.activeText) then
        return
    end
    local n = tab:ActiveCount()
    self.activeText.Text = n .. " active"
    self.activeDot.BackgroundColor3 = n > 0 and T.accent or T.dim
end

function Hub:AddTab(name, iconKind, subtitle)
    local index = #self.tabs + 1
    local tab = setmetatable({
        name = name, subtitle = subtitle or "", sections = {}, controls = {}, featureCount = 0,
    }, Tab)
    local btn = new("TextButton", {
        Size = UDim2.new(1, -16, 0, 36), BackgroundColor3 = T.accentDim, BackgroundTransparency = 1, BorderSizePixel = 0,
        Text = "", AutoButtonColor = false, LayoutOrder = index,
    }, self.sideList)
    corner(btn, 8)
    tab.bar = new("Frame", {
        Size = UDim2.fromOffset(3, 18), Position = UDim2.new(0, 0, 0.5, -9), BackgroundColor3 = T.accent,
        BorderSizePixel = 0, Visible = false,
    }, btn)
    corner(tab.bar, 2)
    tab.icon = makeIcon(iconKind, btn)
    tab.label = makeText({
        Position = UDim2.fromOffset(38, 0), Size = UDim2.new(1, -70, 1, 0), Text = name, Font = Enum.Font.GothamMedium,
        TextSize = 14, TextColor3 = T.dim,
    }, btn)
    tab.countLabel = makeText({
        AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -12, 0, 0), Size = UDim2.fromOffset(24, 36), Text = "0",
        TextXAlignment = Enum.TextXAlignment.Right, Font = Enum.Font.RobotoMono, TextSize = 11, TextColor3 = T.dim,
    }, btn)
    tab.btn = btn
    btn.MouseEnter:Connect(function()
        if self.current ~= tab then
            tween(btn, 0.1, { BackgroundColor3 = T.card, BackgroundTransparency = 0 })
        end
    end)
    btn.MouseLeave:Connect(function()
        if self.current ~= tab then
            tween(btn, 0.1, { BackgroundTransparency = 1 })
        end
    end)
    btn.MouseButton1Click:Connect(function()
        self:Select(tab)
    end)

    tab.page = new("ScrollingFrame", {
        Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 3,
        ScrollBarImageColor3 = T.line, CanvasSize = UDim2.new(), AutomaticCanvasSize = Enum.AutomaticSize.Y,
        Visible = false,
    }, self.pages)
    new("UIPadding", { PaddingLeft = UDim.new(0, 24), PaddingRight = UDim.new(0, 24), PaddingTop = UDim.new(0, 4),
        PaddingBottom = UDim.new(0, 16) }, tab.page)
    listLayout(tab.page, Enum.FillDirection.Vertical, 0)
    local cols = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1 }, tab.page)
    listLayout(cols, Enum.FillDirection.Horizontal, 12)
    tab.colFrames = {}
    for i = 1, 2 do
        local col = new("Frame", {
            Size = UDim2.new(0.5, -6, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = i,
        }, cols)
        listLayout(col, Enum.FillDirection.Vertical, 12)
        tab.colFrames[i] = col
    end
    self.tabs[index] = tab
    return tab
end

function Hub:Select(tab)
    if self.current == tab then
        return
    end
    self:_closePopup()
    self.current = tab
    for _, t in ipairs(self.tabs) do
        local on = t == tab
        t.page.Visible = on
        t.bar.Visible = on
        t.btn.BackgroundColor3 = on and T.accentDim or T.card
        t.btn.BackgroundTransparency = on and 0 or 1
        tween(t.label, 0.15, { TextColor3 = on and T.text or T.dim })
        t.icon.setColor(on and T.accent or T.dim)
        t.countLabel.TextColor3 = on and T.accent or T.dim
    end
    self.title.Text = tab.name
    self.subtitle.Text = tab.subtitle
    self.cover.BackgroundTransparency = 0
    tween(self.cover, 0.22, { BackgroundTransparency = 1 })
    self:_updateHeader()
    self:Filter(self.searchBox.Text)
end

function Hub:Filter(text)
    text = (text or ""):lower()
    local tab = self.current
    if not tab then
        return
    end
    for _, sec in ipairs(tab.sections) do
        local any = false
        for _, r in ipairs(sec.rows) do
            local show = text == "" or r.key:find(text, 1, true) ~= nil
            r.frame.Visible = show
            any = any or show
        end
        sec.card.Visible = text == "" or any or sec.titleKey:find(text, 1, true) ~= nil
    end
end

function Hub:ResetTab()
    local tab = self.current
    if not tab then
        return
    end
    for _, c in ipairs(tab.controls) do
        if not c.noReset then
            c.Set(c.default)
        end
    end
    self:Notify(tab.name, "Tab reset")
end

-- window state ----------------------------------------------------------------------------------------
function Hub:_applyBlur()
    if self.blurWanted and not self.blur then
        self.blur = new("BlurEffect", { Size = 12, Enabled = false }, Lighting)
    end
    if self.blur then
        self.blur.Enabled = self.blurWanted and not self.hidden and not self.minimized
    end
end

function Hub:Open()
    self.holder.Visible = true
    self.window.GroupTransparency = 1
    self.openScale.Scale = 0.94
    tween(self.window, 0.3, { GroupTransparency = 0 })
    tween(self.openScale, 0.3, { Scale = 1 }, Enum.EasingStyle.Back)
    self:_applyBlur()
end

function Hub:Minimize()
    if self.minimized then
        return
    end
    self.minimized = true
    self:_closePopup()
    tween(self.window, 0.18, { GroupTransparency = 1 })
    tween(self.openScale, 0.18, { Scale = 0.9 })
    task.delay(0.2, function()
        if self.minimized then
            self.holder.Visible = false
        end
    end)
    self.icon.Visible = not self.hidden
    self.iconScale.Scale = 0.5
    tween(self.iconScale, 0.25, { Scale = 1 }, Enum.EasingStyle.Back)
    self:_applyBlur()
end

function Hub:Restore()
    self.minimized = false
    self.icon.Visible = false
    self:Open()
end

function Hub:SetHidden(hide)
    if hide == self.hidden then
        return
    end
    self.hidden = hide
    self:_closePopup()
    if hide then
        self.holder.Visible = false
        self.icon.Visible = false
    elseif self.minimized then
        self.icon.Visible = true
    else
        self:Open()
    end
    self:_applyBlur()
end

-- config ----------------------------------------------------------------------------------------------
local CONFIG_DIR = "kryven_hub"
local function configPath()
    return CONFIG_DIR .. "/" .. tostring(game.PlaceId) .. ".json"
end

function Hub:SaveConfig()
    if type(writefile) ~= "function" then
        self:Notify("Config", "writefile is not available", true)
        return false
    end
    if type(isfolder) == "function" and type(makefolder) == "function" and not isfolder(CONFIG_DIR) then
        pcall(makefolder, CONFIG_DIR)
    end
    local okEncode, json = pcall(HttpService.JSONEncode, HttpService, self.flags)
    if not okEncode then
        self:Notify("Config", "Could not encode settings", true)
        return false
    end
    local okWrite = pcall(writefile, configPath(), json)
    self:Notify("Config", okWrite and "Saved" or "Save failed", true)
    return okWrite
end

function Hub:LoadConfig(silent)
    if type(isfile) ~= "function" or type(readfile) ~= "function" or not isfile(configPath()) then
        if not silent then
            self:Notify("Config", "No saved config for this place", true)
        end
        return false
    end
    local okRead, raw = pcall(readfile, configPath())
    local okDecode, data = false, nil
    if okRead then
        okDecode, data = pcall(HttpService.JSONDecode, HttpService, raw)
    end
    if not (okDecode and type(data) == "table") then
        if not silent then
            self:Notify("Config", "Config file is unreadable", true)
        end
        return false
    end
    if silent and data.autoload == false then
        return false
    end
    for flag, value in pairs(data) do
        local c = self.controls[flag]
        if c then
            safe(c.Set, value)
        end
    end
    if not silent then
        self:Notify("Config", "Loaded", true)
    end
    return true
end

-- window construction ---------------------------------------------------------------------------------
function Hub:Build(gameName)
    local holder = new("Frame", {
        Name = "Holder", Size = UDim2.fromOffset(W, H), AnchorPoint = Vector2.new(0.5, 0.5),
        Position = UDim2.fromScale(0.5, 0.5), BackgroundTransparency = 1, Visible = false,
    }, gui)
    self.holder = holder
    self.userScale = new("UIScale", { Scale = 1 }, holder)
    local window = new("CanvasGroup", {
        Size = UDim2.fromScale(1, 1), BackgroundColor3 = T.bg, BorderSizePixel = 0, GroupTransparency = 1,
    }, holder)
    corner(window, 12)
    stroke(window, T.line, 1)
    self.window = window
    self.openScale = new("UIScale", { Scale = 0.94 }, window)

    -- top bar
    local top = new("Frame", { Size = UDim2.new(1, 0, 0, TOP), BackgroundTransparency = 1, Active = true }, window)
    new("Frame", { Size = UDim2.new(1, 0, 0, 1), Position = UDim2.new(0, 0, 1, -1), BackgroundColor3 = T.line, BorderSizePixel = 0 }, top)
    local logo = new("Frame", { Size = UDim2.fromOffset(28, 28), Position = UDim2.fromOffset(16, 12), BackgroundColor3 = T.accent, BorderSizePixel = 0 }, top)
    corner(logo, 7)
    makeText({ Size = UDim2.fromScale(1, 1), Text = "K", Font = Enum.Font.GothamBold, TextSize = 16,
        TextColor3 = T.accentText, TextXAlignment = Enum.TextXAlignment.Center }, logo)
    makeText({ Position = UDim2.fromOffset(54, 0), Size = UDim2.fromOffset(70, TOP), Text = "Kryven",
        Font = Enum.Font.GothamBold, TextSize = 17 }, top)
    local badge = new("Frame", { Position = UDim2.fromOffset(124, 16), Size = UDim2.fromOffset(66, 20), BackgroundColor3 = T.accentDim, BorderSizePixel = 0 }, top)
    corner(badge, 5)
    stroke(badge, T.accent, 1)
    makeText({ Size = UDim2.fromScale(1, 1), Text = "PREMIUM", Font = Enum.Font.RobotoMono, TextSize = 10,
        TextColor3 = T.accent, TextXAlignment = Enum.TextXAlignment.Center }, badge)
    makeText({ Position = UDim2.fromOffset(200, 0), Size = UDim2.fromOffset(12, TOP), Text = "/", TextColor3 = T.dim }, top)
    makeText({ Position = UDim2.fromOffset(216, 0), Size = UDim2.fromOffset(190, TOP), Text = gameName,
        TextColor3 = T.text, TextSize = 14 }, top)

    local searchFrame = new("Frame", {
        Position = UDim2.fromOffset(W - 16 - 28 - 8 - 28 - 8 - 180, 11), Size = UDim2.fromOffset(180, 30),
        BackgroundColor3 = T.panel, BorderSizePixel = 0,
    }, top)
    corner(searchFrame, 8)
    stroke(searchFrame, T.line, 1)
    local lens = new("Frame", { Position = UDim2.fromOffset(10, 9), Size = UDim2.fromOffset(11, 11), BackgroundTransparency = 1 }, searchFrame)
    corner(lens, 6)
    stroke(lens, T.dim, 1.5)
    self.searchBox = new("TextBox", {
        Position = UDim2.fromOffset(30, 0), Size = UDim2.new(1, -38, 1, 0), BackgroundTransparency = 1, Text = "",
        PlaceholderText = "Search features", PlaceholderColor3 = T.dim, TextColor3 = T.text, Font = Enum.Font.Gotham,
        TextSize = 12, TextXAlignment = Enum.TextXAlignment.Left, ClearTextOnFocus = false,
    }, searchFrame)
    bind(self.searchBox:GetPropertyChangedSignal("Text"), function()
        self:Filter(self.searchBox.Text)
    end)

    local function topButton(text, x, onClick)
        local b = new("TextButton", {
            Position = UDim2.fromOffset(x, 12), Size = UDim2.fromOffset(28, 28), BackgroundColor3 = T.panel, BorderSizePixel = 0,
            Text = text, TextColor3 = T.dim, Font = Enum.Font.GothamBold, TextSize = 14, AutoButtonColor = false,
        }, top)
        corner(b, 8)
        b.MouseEnter:Connect(function() tween(b, 0.1, { BackgroundColor3 = T.hover, TextColor3 = T.text }) end)
        b.MouseLeave:Connect(function() tween(b, 0.1, { BackgroundColor3 = T.panel, TextColor3 = T.dim }) end)
        b.MouseButton1Click:Connect(function() safe(onClick) end)
    end
    topButton("-", W - 16 - 28 - 8 - 28, function() self:Minimize() end)
    topButton("x", W - 16 - 28, function()
        self:SetHidden(true)
        self:Notify("Kryven", "Press " .. self.toggleKey.Name .. " to show the hub", true)
    end)
    draggable(top, holder)

    -- sidebar
    local side = new("Frame", { Position = UDim2.fromOffset(0, TOP), Size = UDim2.fromOffset(SIDE, H - TOP - STATUS), BackgroundTransparency = 1 }, window)
    new("Frame", { Size = UDim2.new(0, 1, 1, 0), Position = UDim2.new(1, -1, 0, 0), BackgroundColor3 = T.line, BorderSizePixel = 0 }, side)
    makeText({ Position = UDim2.fromOffset(18, 14), Size = UDim2.fromOffset(120, 16), Text = "FEATURES",
        Font = Enum.Font.RobotoMono, TextSize = 10, TextColor3 = T.dim }, side)
    self.sideList = new("Frame", { Position = UDim2.fromOffset(8, 38), Size = UDim2.new(1, -17, 1, -110), BackgroundTransparency = 1 }, side)
    listLayout(self.sideList, Enum.FillDirection.Vertical, 4)

    local userCard = new("Frame", {
        Position = UDim2.new(0, 8, 1, -60), Size = UDim2.new(1, -17, 0, 52), BackgroundColor3 = T.card, BorderSizePixel = 0,
    }, side)
    corner(userCard, 10)
    stroke(userCard, T.line, 1)
    local avatar = new("ImageLabel", {
        Position = UDim2.fromOffset(10, 10), Size = UDim2.fromOffset(32, 32), BackgroundColor3 = T.panel, BorderSizePixel = 0, Image = "",
    }, userCard)
    corner(avatar, 16)
    task.spawn(function()
        local ok, img = pcall(Players.GetUserThumbnailAsync, Players, lp.UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size100x100)
        if ok and avatar.Parent then
            avatar.Image = img
        end
    end)
    makeText({ Position = UDim2.fromOffset(52, 9), Size = UDim2.new(1, -60, 0, 18), Text = lp.DisplayName,
        Font = Enum.Font.GothamMedium, TextSize = 12 }, userCard)
    makeText({ Position = UDim2.fromOffset(52, 28), Size = UDim2.new(1, -60, 0, 14), Text = "@" .. lp.Name,
        Font = Enum.Font.RobotoMono, TextSize = 10, TextColor3 = T.dim }, userCard)

    -- content header + pages
    local content = new("Frame", {
        Position = UDim2.fromOffset(SIDE, TOP), Size = UDim2.fromOffset(W - SIDE, H - TOP - STATUS), BackgroundTransparency = 1,
    }, window)
    self.title = makeText({ Position = UDim2.fromOffset(24, 12), Size = UDim2.fromOffset(260, 28), Text = "",
        Font = Enum.Font.GothamBold, TextSize = 22 }, content)
    self.subtitle = makeText({ Position = UDim2.fromOffset(24, 40), Size = UDim2.fromOffset(300, 16), Text = "",
        TextSize = 12, TextColor3 = T.dim }, content)
    local contentW = W - SIDE
    local reset = new("TextButton", {
        Position = UDim2.fromOffset(contentW - 24 - 78, 18), Size = UDim2.fromOffset(78, 28), BackgroundColor3 = T.panel,
        BorderSizePixel = 0, Text = "Reset tab", TextColor3 = T.text, Font = Enum.Font.GothamMedium, TextSize = 12,
        AutoButtonColor = false,
    }, content)
    corner(reset, 8)
    stroke(reset, T.line, 1)
    reset.MouseEnter:Connect(function() tween(reset, 0.1, { BackgroundColor3 = T.hover }) end)
    reset.MouseLeave:Connect(function() tween(reset, 0.1, { BackgroundColor3 = T.panel }) end)
    reset.MouseButton1Click:Connect(function() self:ResetTab() end)
    local pill = new("Frame", {
        Position = UDim2.fromOffset(contentW - 24 - 78 - 8 - 92, 18), Size = UDim2.fromOffset(92, 28), BackgroundColor3 = T.panel,
        BorderSizePixel = 0,
    }, content)
    corner(pill, 8)
    stroke(pill, T.line, 1)
    self.activeDot = new("Frame", { Position = UDim2.fromOffset(12, 10), Size = UDim2.fromOffset(8, 8), BackgroundColor3 = T.dim, BorderSizePixel = 0 }, pill)
    corner(self.activeDot, 4)
    self.activeText = makeText({ Position = UDim2.fromOffset(26, 0), Size = UDim2.new(1, -30, 1, 0), Text = "0 active", TextSize = 12 }, pill)

    self.pages = new("Frame", { Position = UDim2.fromOffset(0, 64), Size = UDim2.new(1, 0, 1, -64), BackgroundTransparency = 1 }, content)
    self.cover = new("Frame", {
        Position = UDim2.fromOffset(0, 64), Size = UDim2.new(1, 0, 1, -64), BackgroundColor3 = T.bg, BorderSizePixel = 0,
        BackgroundTransparency = 1, ZIndex = 10,
    }, content)

    -- status bar
    local status = new("Frame", { Position = UDim2.new(0, 0, 1, -STATUS), Size = UDim2.new(1, 0, 0, STATUS), BackgroundTransparency = 1 }, window)
    new("Frame", { Size = UDim2.new(1, 0, 0, 1), BackgroundColor3 = T.line, BorderSizePixel = 0 }, status)
    local dot = new("Frame", { Position = UDim2.fromOffset(18, 11), Size = UDim2.fromOffset(8, 8), BackgroundColor3 = T.accent, BorderSizePixel = 0 }, status)
    corner(dot, 4)
    local left = makeText({ Position = UDim2.fromOffset(34, 0), Size = UDim2.new(0.5, 0, 1, 0), Font = Enum.Font.RobotoMono,
        TextSize = 11, TextColor3 = T.dim, Text = "" }, status)
    local right = makeText({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -18, 0, 0), Size = UDim2.new(0.5, 0, 1, 0),
        TextXAlignment = Enum.TextXAlignment.Right, Font = Enum.Font.RobotoMono, TextSize = 11, TextColor3 = T.dim, Text = "" }, status)
    local frames, fps, started = 0, 0, os.clock()
    bind(RunService.Heartbeat, function()
        frames = frames + 1
    end)
    task.spawn(function()
        while State.alive do
            fps, frames = frames, 0
            local ping = "?"
            local ok, value = pcall(function()
                return Stats.Network.ServerStatsItem["Data Ping"]:GetValue()
            end)
            if ok and value then
                ping = tostring(math.floor(value + 0.5))
            end
            local elapsed = math.floor(os.clock() - started)
            left.Text = string.format("Connected    Place %s    Server %d/%d", tostring(game.PlaceId), #Players:GetPlayers(), Players.MaxPlayers)
            right.Text = string.format("FPS %d    Ping %sms    %02d:%02d:%02d    %s to hide", fps, ping,
                math.floor(elapsed / 3600), math.floor(elapsed / 60) % 60, elapsed % 60, self.toggleKey.Name)
            task.wait(1)
        end
    end)

    -- toasts
    self.toastHolder = new("Frame", {
        Size = UDim2.fromOffset(260, 320), AnchorPoint = Vector2.new(1, 1), Position = UDim2.new(1, -20, 1, -20), BackgroundTransparency = 1,
    }, gui)
    new("UIListLayout", {
        VerticalAlignment = Enum.VerticalAlignment.Bottom, HorizontalAlignment = Enum.HorizontalAlignment.Right,
        Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder,
    }, self.toastHolder)

    -- minimized icon
    local icon = new("Frame", {
        Name = "Icon", Size = UDim2.fromOffset(52, 52), Position = UDim2.fromOffset(24, 90), BackgroundColor3 = T.panel,
        BorderSizePixel = 0, Visible = false, Active = true,
    }, gui)
    corner(icon, 14)
    local iconStroke = stroke(icon, T.line, 1)
    self.iconScale = new("UIScale", { Scale = 1 }, icon)
    local iconK = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(30, 30),
        BackgroundColor3 = T.accent, BorderSizePixel = 0 }, icon)
    corner(iconK, 8)
    makeText({ Size = UDim2.fromScale(1, 1), Text = "K", Font = Enum.Font.GothamBold, TextSize = 18, TextColor3 = T.accentText,
        TextXAlignment = Enum.TextXAlignment.Center }, iconK)
    icon.MouseEnter:Connect(function() tween(iconStroke, 0.12, { Color = T.accent }) end)
    icon.MouseLeave:Connect(function() tween(iconStroke, 0.12, { Color = T.line }) end)
    draggable(icon, icon, function() self:Restore() end)
    self.icon = icon

    -- keyboard toggle
    bind(UserInputService.InputBegan, function(input, processed)
        if processed or self.listening then
            return
        end
        if input.KeyCode == self.toggleKey then
            self:SetHidden(not self.hidden)
        end
    end)
end

-- splash ----------------------------------------------------------------------------------------------
local function runSplash(gameName, steps)
    local card = new("CanvasGroup", {
        Size = UDim2.fromOffset(380, 200), AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5),
        BackgroundColor3 = T.panel, BorderSizePixel = 0, GroupTransparency = 1,
    }, gui)
    corner(card, 14)
    stroke(card, T.line, 1)
    local cardScale = new("UIScale", { Scale = 0.92 }, card)
    local logo = new("Frame", { Size = UDim2.fromOffset(56, 56), Position = UDim2.new(0.5, -28, 0, 26), BackgroundColor3 = T.accent, BorderSizePixel = 0 }, card)
    corner(logo, 14)
    makeText({ Size = UDim2.fromScale(1, 1), Text = "K", Font = Enum.Font.GothamBold, TextSize = 30, TextColor3 = T.accentText,
        TextXAlignment = Enum.TextXAlignment.Center }, logo)
    local logoScale = new("UIScale", {}, logo)
    local pulse = TweenService:Create(logoScale, TweenInfo.new(0.7, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, -1, true), { Scale = 1.1 })
    pulse:Play()
    makeText({ Position = UDim2.fromOffset(0, 92), Size = UDim2.new(1, 0, 0, 28), Text = "Kryven", Font = Enum.Font.GothamBold,
        TextSize = 22, TextXAlignment = Enum.TextXAlignment.Center }, card)
    makeText({ Position = UDim2.fromOffset(0, 120), Size = UDim2.new(1, 0, 0, 16), Text = gameName, TextSize = 12,
        TextColor3 = T.dim, TextXAlignment = Enum.TextXAlignment.Center }, card)
    local track = new("Frame", { Position = UDim2.fromOffset(32, 154), Size = UDim2.new(1, -64, 0, 4), BackgroundColor3 = T.off, BorderSizePixel = 0 }, card)
    corner(track, 2)
    local fill = new("Frame", { Size = UDim2.fromScale(0, 1), BackgroundColor3 = T.accent, BorderSizePixel = 0 }, track)
    corner(fill, 2)
    local statusLabel = makeText({ Position = UDim2.fromOffset(32, 166), Size = UDim2.new(1, -110, 0, 16), Text = "",
        Font = Enum.Font.RobotoMono, TextSize = 11, TextColor3 = T.dim }, card)
    local percent = makeText({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -32, 0, 166), Size = UDim2.fromOffset(60, 16),
        Text = "0%", TextXAlignment = Enum.TextXAlignment.Right, Font = Enum.Font.RobotoMono, TextSize = 11, TextColor3 = T.dim }, card)

    tween(card, 0.35, { GroupTransparency = 0 })
    tween(cardScale, 0.35, { Scale = 1 }, Enum.EasingStyle.Back)
    task.wait(0.35)
    for i, step in ipairs(steps) do
        statusLabel.Text = step[1] .. "..."
        local ok, err = pcall(step[2])
        if not ok then
            warn("[kryven] step '" .. step[1] .. "' failed: " .. tostring(err))
        end
        tween(fill, 0.3, { Size = UDim2.fromScale(i / #steps, 1) })
        percent.Text = math.floor(i / #steps * 100) .. "%"
        task.wait(0.3)
    end
    statusLabel.Text = "Ready"
    task.wait(0.25)
    tween(card, 0.3, { GroupTransparency = 1 })
    tween(cardScale, 0.3, { Scale = 1.06 })
    task.wait(0.32)
    pulse:Cancel()
    card:Destroy()
end

-- ===================================================================================================
-- Part 4: features wired to the hub
-- ===================================================================================================

local function describe(result, extra)
    if result == "no_target" then
        return string.format("no target (%d seen, %d filtered, %s)", extra.total, extra.filtered, extra.orderUsed)
    elseif result == "source_unavailable" then
        return "world not ready: " .. tostring(extra)
    elseif result == "paused" then
        return string.format("paused %.0fs: %s", extra.remaining, tostring(extra.reason))
    elseif result == "deposited" then
        local parts = { string.format("placed %d", extra.placed) }
        if extra.kept > 0 then
            parts[#parts + 1] = string.format("kept %d (pen refused)", extra.kept)
        end
        if extra.sold > 0 then
            parts[#parts + 1] = "sold " .. extra.sold
        end
        if extra.lost > 0 then
            parts[#parts + 1] = "lost " .. extra.lost
        end
        return table.concat(parts, ", ")
    elseif result == "place_failed" then
        return "place failed"
    elseif type(extra) == "table" and extra.name then
        return result .. ": " .. extra.name
    end
    return result
end

local function entryCount(attrName)
    local raw = lp:GetAttribute(attrName)
    if type(raw) ~= "string" then
        return "?"
    end
    local ok, decoded = pcall(HttpService.JSONDecode, HttpService, raw)
    return (ok and type(decoded) == "table") and #decoded or "?"
end

-- One-line snapshot of what could make a placement fail (pen full, Exu not in hand, ...)
local function placeDiagnostics()
    local pen = ownPen()
    local exusFolder = pen and pen:FindFirstChild("Exus")
    local tools = 0
    for _ in pairs(toolIds()) do
        tools = tools + 1
    end
    return string.format("carried=%s | Capacity=%s PenLevel=%s PenEntries=%s PenExusFolder=%s InvEntries=%s Tools=%d",
        table.concat(ctl.carried, ","), tostring(lp:GetAttribute("Capacity")), tostring(lp:GetAttribute("PenLevel")),
        tostring(entryCount("Pen")), exusFolder and tostring(#exusFolder:GetChildren()) or "?",
        tostring(entryCount("Inv")), tools)
end

local function teleportTo(x, y, z)
    local _, _, hrp = character()
    if not hrp then
        Hub:Notify("Teleport", "Character not ready", true)
        return
    end
    hrp.CFrame = CFrame.new(x, y, z)
    hrp.AssemblyLinearVelocity = Vector3.zero
end

-- area lists (Farm: pick which areas to steal from; Teleport: jump to an area) ---------------------------
local areaViews = {}

local function areaRows(container, mode)
    local lastSignature
    local function render()
        local areas = ctl.areas
        local rows, parts = {}, {}
        if areas then
            for i, a in ipairs(areas.list) do
                local rar = {}
                for _, r in ipairs(a.rarities) do
                    rar[#rar + 1] = r
                end
                local selected = mode == "select" and Core.isAreaSelected(ctl, i)
                rows[#rows + 1] = {
                    index = i, selected = selected,
                    title = string.format("Area %d%s", i, i == #areas.list and "  (last)" or ""),
                    detail = string.format("%d alive  |  %s  |  @%d,%d", a.alive, #rar > 0 and table.concat(rar, "/") or "-",
                        math.floor(a.centroid.x + 0.5), math.floor(a.centroid.z + 0.5)),
                    centroid = a.centroid,
                }
                parts[#parts + 1] = rows[#rows].title .. rows[#rows].detail .. tostring(selected)
            end
        end
        local signature = table.concat(parts, "\n")
        if signature == lastSignature then
            return
        end
        lastSignature = signature
        for _, child in ipairs(container:GetChildren()) do
            if child:IsA("TextButton") or child:IsA("TextLabel") then
                child:Destroy()
            end
        end
        if #rows == 0 then
            makeText({ Size = UDim2.new(1, 0, 0, 24), Text = "No Exus seen yet", TextSize = 12, TextColor3 = T.dim }, container)
            return
        end
        for _, r in ipairs(rows) do
            local b = new("TextButton", {
                Size = UDim2.new(1, 0, 0, 40), BackgroundColor3 = r.selected and T.accentDim or T.panel, BorderSizePixel = 0,
                Text = "", AutoButtonColor = false, LayoutOrder = r.index,
            }, container)
            corner(b, 8)
            stroke(b, r.selected and T.accent or T.line, 1)
            if mode == "select" then
                local box = new("Frame", {
                    Position = UDim2.fromOffset(10, 13), Size = UDim2.fromOffset(14, 14),
                    BackgroundColor3 = r.selected and T.accent or T.off, BorderSizePixel = 0,
                }, b)
                corner(box, 4)
            end
            makeText({ Position = UDim2.fromOffset(mode == "select" and 34 or 12, 4), Size = UDim2.new(1, -44, 0, 17), Text = r.title,
                Font = Enum.Font.GothamMedium, TextSize = 12 }, b)
            makeText({ Position = UDim2.fromOffset(mode == "select" and 34 or 12, 21), Size = UDim2.new(1, -44, 0, 14), Text = r.detail,
                Font = Enum.Font.RobotoMono, TextSize = 10, TextColor3 = T.dim }, b)
            b.MouseButton1Click:Connect(function()
                if mode == "select" then
                    Core.toggleArea(ctl, r.index)
                    for _, view in ipairs(areaViews) do
                        view.render()
                    end
                else
                    teleportTo(r.centroid.x, r.centroid.y + 5, r.centroid.z)
                    Hub:Notify("Teleport", r.title)
                end
            end)
        end
    end
    areaViews[#areaViews + 1] = { render = render }
    render()
end

-- player features -----------------------------------------------------------------------------------------
local Player = { walk = nil, jump = nil, defWalk = 16, defJump = 50, infinite = nil, afk = nil }

local function readDefaults()
    local _, hum = character()
    if hum then
        Player.defWalk = math.floor(hum.WalkSpeed + 0.5)
        Player.defJump = math.floor(hum.JumpPower + 0.5)
    end
end

local function applyPlayerOverrides()
    local _, hum = character()
    if not hum then
        return
    end
    if Player.walk then
        hum.WalkSpeed = Player.walk
    end
    if Player.jump then
        hum.UseJumpPower = true
        hum.JumpPower = Player.jump
    end
end

local function restorePlayer()
    local _, hum = character()
    if hum then
        if Player.walk then
            hum.WalkSpeed = Player.defWalk
        end
        if Player.jump then
            hum.JumpPower = Player.defJump
        end
    end
end

-- main loop -----------------------------------------------------------------------------------------------
local live = {}

local function runLoop()
    local lastHaltCount = 0
    local lastTarget = "-"
    local IDLE = { no_target = true, source_unavailable = true, player_unavailable = true, paused = true, aborted = true }

    local function step()
        local _, hum = character()
        ctl.cfg.select.speed = (CFG.moveTarget == "teleport") and CFG.teleportSpeed or (hum and hum.WalkSpeed or nil)
        ctl.cfg.returnInstant = CFG.moveBase == "teleport"
        local result, extra = Core.runOnce(ctl)
        if result == "place_failed" or (result == "deposited" and extra.kept > 0 and extra.detail) then
            print("[kryven] placement refused | " .. tostring(extra.detail) .. " | " .. placeDiagnostics())
        end
        if ctl.haltCount ~= lastHaltCount then
            lastHaltCount = ctl.haltCount
            if ctl.haltCount > 0 then
                print(string.format("[kryven] halt #%d: %s", ctl.haltCount, tostring(ctl.lastHalt)))
            end
        end
        if type(extra) == "table" and extra.name and extra.pos then
            local idx = ctl.areas and ctl.areas.byKey[Core.pointKey(extra.pos)]
            lastTarget = extra.name .. (idx and (" (area " .. idx .. ")") or "")
        end
        local text = describe(result, extra)
        if ctl.stopReason then
            print("[kryven] stopped: " .. ctl.stopReason)
            text = "STOPPED: " .. ctl.stopReason
            if live.autoToggle then
                live.autoToggle.Set(false)
            else
                State.enabled = false
            end
        end
        if live.state then
            live.state.Set(text)
            live.target.Set(lastTarget)
            live.counts.Set(string.format("%d / %d / %d / %d", ctl.stolen, ctl.placed, ctl.kept, ctl.sold))
            if result == "deposited" then
                local function ms(a, b)
                    return (a ~= 0 and b ~= 0) and string.format("%.0f", (b - a) * 1000) or "?"
                end
                live.timing.Set(string.format("%s / %s / %s ms", ms(timeline.tTrigger, timeline.tTool),
                    ms(timeline.tTrigger, timeline.tTp), ms(timeline.tTp, timeline.tPlaced)))
            end
        end
        return result
    end

    while State.alive do
        if not State.enabled then
            task.wait(0.2)
        else
            -- an uncaught error would end this thread silently while the toggle still looks ON
            local ok, result = pcall(step)
            if not ok then
                warn("[kryven] loop error: " .. tostring(result))
                if live.state then
                    live.state.Set("error: " .. tostring(result))
                end
                task.wait(1)
            else
                task.wait(IDLE[result] and CFG.idleDelay or CFG.loopDelay)
            end
        end
    end
end

local function buildTabs()
    local farm = Hub:AddTab("Farm", "diamond", "Auto steal, pen and areas")

    local sSteal = farm:Section("AUTO STEAL", 1)
    live.autoToggle = sSteal:Toggle({
        name = "Auto Steal", desc = "Steals Exus and brings them to your pen", flag = "autosteal", noSave = true, noReset = false,
        callback = function(v)
            State.enabled = v
            if v then
                Core.reset(ctl)
            end
        end,
    })
    sSteal:Toggle({
        name = "Return and place", desc = "Goes home after each steal and places the Exu", flag = "deposit", default = true,
        callback = function(v) ctl.cfg.deposit = v end,
    })
    sSteal:Dropdown({
        name = "If the pen is full", desc = "Sell is permanent", flag = "onfull", options = { "Keep in inventory", "Sell", "Stop" },
        default = "Keep in inventory",
        callback = function(v)
            ctl.cfg.onPlaceFail = (v == "Sell" and "sell") or (v == "Stop" and "stop") or "keep"
        end,
    })
    sSteal:Toggle({
        name = "Escape on steal", desc = "Teleports home the instant the steal fires", flag = "escape", default = true,
        callback = function(v) ctl.cfg.escapeOnTrigger = v end,
    })
    sSteal:Slider({
        name = "Batch", flag = "batch", min = 1, max = 5, default = 1,
        callback = function(v) ctl.cfg.batch = v end,
    })

    local sMove = farm:Section("MOVEMENT", 1)
    sMove:Dropdown({
        name = "Go to Exu", flag = "moveTarget", options = { "Teleport", "Walk" }, default = "Teleport",
        callback = function(v) CFG.moveTarget = v:lower() end,
    })
    sMove:Dropdown({
        name = "Return to pen", flag = "moveBase", options = { "Teleport", "Walk" }, default = "Teleport",
        callback = function(v) CFG.moveBase = v:lower() end,
    })
    sMove:Dropdown({
        name = "Steal method", flag = "hold", options = { "Instant", "Hold" }, default = "Instant",
        callback = function(v) CFG.hold = v == "Instant" and "fire" or "hold" end,
    })
    sMove:Slider({
        name = "Teleport settle", flag = "settle", min = 0, max = 400, step = 10, default = 120,
        format = function(v) return v .. "ms" end,
        callback = function(v) CFG.settleSeconds = v / 1000 end,
    })

    local sTarget = farm:Section("TARGETING", 2)
    sTarget:Dropdown({
        name = "Priority", flag = "priority", options = { "Efficiency", "Highest Need", "Nearest" }, default = "Efficiency",
        callback = function(v)
            ctl.cfg.select.order = (v == "Efficiency" and "efficiency") or (v == "Nearest" and "nearest") or "value"
        end,
    })
    sTarget:Dropdown({
        name = "Area order", desc = "With several areas selected", flag = "areaorder",
        options = { "Best area first", "Nearest area first", "Off" }, default = "Best area first",
        callback = function(v)
            ctl.cfg.areaPriority = (v == "Best area first" and "best") or (v == "Nearest area first" and "near") or false
        end,
    })
    sTarget:Slider({
        name = "Max range", flag = "range", min = 50, max = 2000, step = 50, default = 2000,
        format = function(v) return v >= 2000 and "any" or tostring(v) end,
        callback = function(v) ctl.cfg.select.maxRange = v >= 2000 and math.huge or v end,
    })

    local sAreas = farm:Section("AREAS", 2)
    sAreas:Buttons({
        name = "Quick select", buttons = {
            { text = "All", callback = function() Core.selectAll(ctl) for _, v in ipairs(areaViews) do v.render() end end },
            { text = "Last", accent = true, callback = function() Core.selectLast(ctl) for _, v in ipairs(areaViews) do v.render() end end },
        },
    })
    areaRows(sAreas:Container("areas"), "select")
    sAreas:Buttons({
        name = "Forget spawn points", desc = "Use after the map changed", buttons = {
            { text = "Reset", callback = function() Core.clearAreas(ctl) for _, v in ipairs(areaViews) do v.render() end end },
        },
    })

    local sStatus = farm:Section("STATUS", 2)
    live.state = sStatus:Info({ name = "State", value = "idle" })
    live.target = sStatus:Info({ name = "Last target", value = "-" })
    live.counts = sStatus:Info({ name = "Stolen / placed / kept / sold", value = "0 / 0 / 0 / 0" })
    live.timing = sStatus:Info({ name = "Trigger>Tool / TP / place", value = "-" })

    -- Player -------------------------------------------------------------------------------------------
    local player = Hub:AddTab("Player", "dot", "Movement and utility")
    local sMoveP = player:Section("MOVEMENT", 1)
    sMoveP:Slider({
        name = "WalkSpeed", flag = "walkspeed", min = 8, max = 200, default = Player.defWalk,
        callback = function(v)
            Player.walk = (math.abs(v - Player.defWalk) < 0.5) and nil or v
            if not Player.walk then
                local _, hum = character()
                if hum then hum.WalkSpeed = Player.defWalk end
            end
        end,
    })
    sMoveP:Slider({
        name = "JumpPower", flag = "jumppower", min = 20, max = 300, default = math.clamp(Player.defJump, 20, 300),
        callback = function(v)
            Player.jump = (math.abs(v - Player.defJump) < 0.5) and nil or v
            if not Player.jump then
                local _, hum = character()
                if hum then hum.JumpPower = Player.defJump end
            end
        end,
    })
    sMoveP:Toggle({
        name = "Infinite Jump", flag = "infjump",
        callback = function(v)
            if v and not Player.infinite then
                Player.infinite = UserInputService.JumpRequest:Connect(function()
                    local _, hum = character()
                    if hum then
                        hum:ChangeState(Enum.HumanoidStateType.Jumping)
                    end
                end)
                connections[#connections + 1] = Player.infinite
            elseif not v and Player.infinite then
                Player.infinite:Disconnect()
                Player.infinite = nil
            end
        end,
    })
    local sMisc = player:Section("MISC", 2)
    sMisc:Toggle({
        name = "Anti AFK", desc = "Prevents idle kick", flag = "antiafk", default = true,
        callback = function(v)
            if v and not Player.afk then
                Player.afk = lp.Idled:Connect(function()
                    local vu = game:GetService("VirtualUser")
                    vu:CaptureController()
                    vu:ClickButton2(Vector2.new(0, 0))
                end)
                connections[#connections + 1] = Player.afk
            elseif not v and Player.afk then
                Player.afk:Disconnect()
                Player.afk = nil
            end
        end,
    })

    -- Teleport -----------------------------------------------------------------------------------------
    local tp = Hub:AddTab("Teleport", "ring", "Pen and farming areas")
    local sBase = tp:Section("PEN", 1)
    sBase:Buttons({
        name = "Teleport to pen", buttons = {
            { text = "Go", accent = true, callback = function()
                local base = Source.basePos()
                if base then
                    teleportTo(base.x, base.y + 4, base.z)
                    Hub:Notify("Teleport", "Pen")
                else
                    Hub:Notify("Teleport", "Pen not found", true)
                end
            end },
        },
    })
    local sTpAreas = tp:Section("AREAS", 2)
    areaRows(sTpAreas:Container("areas"), "teleport")

    -- Settings -----------------------------------------------------------------------------------------
    local settings = Hub:AddTab("Settings", "bars", "Interface, configs and about")
    local sUi = settings:Section("INTERFACE", 1)
    sUi:Keybind({
        name = "Toggle UI", flag = "togglekey", default = "RightShift", noReset = true,
        callback = function(v) Hub.toggleKey = keyCode(v) or Enum.KeyCode.RightShift end,
    })
    sUi:Slider({
        name = "UI scale", flag = "uiscale", min = 60, max = 140, step = 5, default = 100,
        format = function(v) return v .. "%" end,
        callback = function(v) Hub.userScale.Scale = v / 100 end,
    })
    sUi:Toggle({
        name = "Blur background", flag = "blur", notify = false,
        callback = function(v) Hub.blurWanted = v Hub:_applyBlur() end,
    })
    sUi:Toggle({
        name = "Notifications", desc = "Shows a toast when a feature changes", flag = "notifications", default = true, notify = false,
        callback = function(v) Hub.notifications = v end,
    })

    local sCfg = settings:Section("CONFIG", 2)
    sCfg:Toggle({ name = "Auto load on join", flag = "autoload", default = true, notify = false })
    sCfg:Buttons({ name = "Save current config", buttons = { { text = "Save", callback = function() Hub:SaveConfig() end } } })
    sCfg:Buttons({ name = "Load config", buttons = { { text = "Load", callback = function() Hub:LoadConfig(false) end } } })

    local sAbout = settings:Section("ABOUT", 2)
    sAbout:Info({ name = "Version", value = VERSION })
    sAbout:Info({ name = "Game", value = Hub.gameName })
    sAbout:Info({ name = "Place", value = tostring(game.PlaceId) })
    local executor = "unknown"
    if type(identifyexecutor) == "function" then
        local ok, name = pcall(identifyexecutor)
        if ok and name then
            executor = tostring(name)
        end
    end
    sAbout:Info({ name = "Executor", value = executor })
    sAbout:Buttons({ name = "Unload hub", desc = "Stops everything and removes the UI", buttons = {
        { text = "Unload", danger = true, callback = function() env.__KryvenUnload() end },
    } })
end

-- unload ------------------------------------------------------------------------------------------------------
local function unload()
    if not State.alive then
        return
    end
    State.alive, State.enabled = false, false
    for _, c in ipairs(connections) do
        pcall(function() c:Disconnect() end)
    end
    connections = {}
    pcall(restorePlayer)
    if Hub.blur then
        pcall(function() Hub.blur:Destroy() end)
    end
    pcall(function() gui:Destroy() end)
    if env.__KryvenUnload == unload then
        env.__KryvenUnload = nil
    end
end
env.__KryvenUnload = unload

-- boot ----------------------------------------------------------------------------------------------------------
if not mountGui() then
    warn("[kryven] could not parent the ScreenGui; nothing started")
    unload()
    return Core
end

local gameName = "Steal An Exu"
do
    local fetched
    task.spawn(function()
        local ok, info = pcall(MarketplaceService.GetProductInfo, MarketplaceService, game.PlaceId)
        fetched = ok and info and info.Name or false
    end)
    local deadline = os.clock() + 1.5
    while fetched == nil and os.clock() < deadline do
        task.wait()
    end
    if type(fetched) == "string" and fetched ~= "" then
        gameName = fetched
    end
end
Hub.gameName = gameName

runSplash(gameName, {
    { "Initializing", function() readDefaults() end },
    { "Reading game data", function()
        local snaps = Source.snapshot()
        if snaps then
            Core.observe(ctl, snaps)
        end
    end },
    { "Building interface", function()
        Hub:Build(gameName)
        buildTabs()
        Hub:Select(Hub.tabs[1])
    end },
    { "Loading config", function()
        Hub:LoadConfig(true)
    end },
})

Hub.ready = true
Hub:Open()
Hub:Notify("Kryven", "Loaded - " .. Hub.toggleKey.Name .. " toggles the hub", true)

task.spawn(runLoop)
task.spawn(function()
    while State.alive do
        applyPlayerOverrides()
        local snaps = Source.snapshot()
        if snaps then
            Core.observe(ctl, snaps)
        end
        for _, view in ipairs(areaViews) do
            view.render()
        end
        task.wait(1)
    end
end)

return Core
Game pageSteal An Exu · 1 script on this siteRoblox ›

More Steal An Exu scripts

See all
Get Delta Script first on GoogleAdd us as a preferred source to see our new scripts in your Google results.
Add Delta Script on Google

Leave a Reply

Your email address will not be published. Required fields are marked *