Item Spawner - Bypass Gamepass [OPEN-SOURCE]
Key Required

Item Spawner - Bypass Gamepass [OPEN-SOURCE]

Brookhaven 🏡RPdevicedevice 682 views 4.6K executesUpdated Oct 7, 2026
5.0/ 5(2 votes)
20100% Positive
Lua Script•1683 lines (85003 chars)
local M = {
    version = "5.2", dead = false, generation = 0, mining = false, scanning = false, deepScanning = false,
    connections = {}, rows = {}, workers = {}, entries = {}, catalog = {}, observed = {},
    categories = {}, logs = {}, filter = "All tools", category = "", query = "",
    page = 1, pageSize = 36, selected = nil, busy = nil, nextRequest = 0,
    source = "Not mined", coverage = "Not mined", rawConfig = nil, remoteTree = nil, deepStats = {},
    handoffDonorName = nil, lastIncomingOffer = nil,
    colors = {
        bg = Color3.fromRGB(18, 22, 28), panel = Color3.fromRGB(26, 32, 40),
        row = Color3.fromRGB(33, 41, 51), active = Color3.fromRGB(44, 72, 91),
        text = Color3.fromRGB(232, 241, 248), muted = Color3.fromRGB(148, 167, 181),
        accent = Color3.fromRGB(136, 211, 237), good = Color3.fromRGB(137, 218, 175),
        bad = Color3.fromRGB(237, 149, 149), line = Color3.fromRGB(60, 76, 91)
    }
}

-- BEGIN CATALOG LOGIC (also exercised by the offline validation harness)
function M.nonempty(value)
    return type(value) == "string" and value ~= ""
end

function M.icon(value)
    if type(value) == "number" or (type(value) == "string" and value:match("^%d+$")) then
        return "rbxassetid://" .. tostring(value)
    end
    return type(value) == "string" and value or ""
end

function M.key(value)
    return tostring(value):gsub("%s+", "-"):upper()
end

function M.configPath(root, path)
    local node = root
    for segment in tostring(path):gmatch("[^/]+") do
        if type(node) ~= "table" then return nil end
        local nextNode = node[segment]
        if nextNode == nil then
            for key, value in pairs(node) do
                if type(key) == "string" and M.key(key) == M.key(segment) then
                    nextNode = value
                    break
                end
            end
        end
        if nextNode == nil then return nil end
        node = nextNode
    end
    return node
end

function M.findConfig(packages, defaultPackage)
    if type(packages) ~= "table" then return nil end
    local roots, seen = {}, {}
    local function add(name, value)
        if type(value) == "table" and not seen[value] then
            seen[value] = true
            roots[#roots + 1] = {name = name, value = value}
        end
    end
    add("brookhaven", packages.brookhaven)
    for key, value in pairs(packages) do
        if type(key) == "string" and key:lower() == "brookhaven" then add(key, value) end
    end
    if type(defaultPackage) == "string" then add(defaultPackage, packages[defaultPackage]) end
    local keys = {}
    for key in pairs(packages) do keys[#keys + 1] = key end
    table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
    for _, key in ipairs(keys) do add(tostring(key), packages[key]) end
    add("root", packages)
    for _, candidate in ipairs(roots) do
        local config = M.configPath(candidate.value, "Tools/Config")
        if type(config) == "table" then return config, candidate.name end
    end
    return nil
end

function M.filterText(value)
    if type(value) == "string" then return value end
    local out = {}
    if type(value) == "table" then
        for key, item in pairs(value) do
            if type(item) == "string" then out[#out + 1] = item
            elseif item == true and type(key) == "string" then out[#out + 1] = key end
        end
    end
    table.sort(out)
    return table.concat(out, ", ")
end

function M.parseConfig(config, source)
    local entries, categories, stats = {}, {}, {rows = 0, tools = 0, categories = 0, skipped = 0, duplicates = 0}
    if type(config) ~= "table" then return entries, categories, stats end
    local keys = {}
    for key in pairs(config) do keys[#keys + 1] = key end
    table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
    for _, key in ipairs(keys) do
        local raw = config[key]
        stats.rows = stats.rows + 1
        if type(raw) == "table" then
            local name = M.nonempty(raw.Name) and raw.Name or (type(key) == "string" and key or nil)
            if name and name ~= "" and name ~= "ClearTools" then
                if entries[name] then
                    stats.duplicates = stats.duplicates + 1
                else
                    local isCategory = raw.IsCategory == true
                    local gun = M.nonempty(raw.GunSkin) and raw.GunSkin or nil
                    local item = M.nonempty(raw.Item) and raw.Item or nil
                    local requirement = raw.RequirementBehaviorData
                    local e = {
                        name = name, displayName = M.nonempty(raw.DisplayName) and raw.DisplayName or name,
                        category = M.nonempty(raw.Category) and raw.Category or "",
                        icon = M.icon(raw.Icon or raw.IconWhenUnlocked),
                        order = tonumber(raw.Order) or 0, isCategory = isCategory,
                        kind = isCategory and "Category" or (gun and "Gun skin" or "Tool"),
                        route = not isCategory and (gun and "GunEquip" or "PickingTools") or nil,
                        gunSkin = gun, item = item, filters = M.filterText(raw.Filter),
                        requirement = type(requirement) == "table" and tostring(requirement.Behavior or "Configured") or "",
                        hiddenUnlessUnlocked = raw.HiddenUnlessUnlocked == true,
                        source = source, raw = raw, catalog = true
                    }
                    entries[name] = e
                    if isCategory then
                        categories[name] = e
                        stats.categories = stats.categories + 1
                    else stats.tools = stats.tools + 1 end
                end
            else stats.skipped = stats.skipped + 1 end
        else stats.skipped = stats.skipped + 1 end
    end
    return entries, categories, stats
end

function M.categoryPath(entry, categories)
    local parts, seen, current = {}, {}, entry.category
    while current and current ~= "" and not seen[current] do
        seen[current] = true
        table.insert(parts, 1, categories[current] and categories[current].displayName or current)
        current = categories[current] and categories[current].category or nil
    end
    return #parts > 0 and table.concat(parts, " / ") or "General"
end

function M.inCategory(entry, wanted, categories)
    if wanted == "" then return true end
    local seen, current = {}, entry.category
    if entry.isCategory and entry.name == wanted then return true end
    while current and current ~= "" and not seen[current] do
        if current == wanted then return true end
        seen[current] = true
        current = categories[current] and categories[current].category or nil
    end
    return false
end

function M.matches(entry, query)
    local haystack = table.concat({entry.name, entry.displayName, entry.kind, entry.category,
        entry.categoryPath or "", entry.filters or "", entry.item or "", entry.source or ""}, " "):lower()
    for word in query:lower():gmatch("%S+") do
        if not haystack:find(word, 1, true) then return false end
    end
    return true
end

function M.requestArgs(entry)
    if entry.isCategory then return nil end
    if entry.route == "GunEquip" and M.nonempty(entry.gunSkin) then
        return "Remotes.GunEquip", {entry.gunSkin, entry.name}
    end
    if entry.route == "PickingTools" then
        return "RE.1Too1l", {"PickingTools", entry.name}
    end
    return nil
end

function M.actionDecision(entry, intent, present, busy)
    if entry.isCategory then return "category" end
    if intent == "hold" and present then return "hold" end
    if intent == "get" and present then return "owned" end
    if intent == "remove" and not present then return "absent" end
    if busy then return "busy" end
    if M.requestArgs(entry) then return "request" end
    if (intent == "get" or intent == "hold") and entry.interactions and #entry.interactions > 0 then return "interaction" end
    return "unavailable"
end
-- END CATALOG LOGIC

M.Players = game:GetService("Players")
M.RS = game:GetService("ReplicatedStorage")
M.Input = game:GetService("UserInputService")
M.Http = game:GetService("HttpService")
M.Tags = game:GetService("CollectionService")
M.Player = M.Players.LocalPlayer
if not M.Player then error("Tool Miner must run on the client.") end
M.PlayerGui = M.Player:WaitForChild("PlayerGui", 15)
if not M.PlayerGui then error("Tool Miner: PlayerGui was not available.") end
M.env = (type(getgenv) == "function" and getgenv()) or _G
if type(M.env.ToolMinerSpawnerUnload) == "function" then pcall(M.env.ToolMinerSpawnerUnload) end
M.started = os.clock()

function M.track(connection, list)
    local target = list or M.connections
    target[#target + 1] = connection
    return connection
end

function M.disconnect(list)
    for _, connection in ipairs(list) do pcall(function() connection:Disconnect() end) end
    table.clear(list)
end

function M.trace(message)
    if M.dead then return end
    local line = string.format("[%6.2fs] %s", os.clock() - M.started, tostring(message))
    M.logs[#M.logs + 1] = line
    if #M.logs > 220 then table.remove(M.logs, 1) end
    print("[ToolMiner] " .. line)
    if M.modalMode == "log" and M.ui and M.ui.exportBox then
        pcall(function() M.ui.exportBox.Text = table.concat(M.logs, "\n") end)
    end
end

function M.status(message, state)
    if M.dead or not M.ui or not M.ui.status then return end
    pcall(function()
        M.ui.status.Text = message
        M.ui.status.TextColor3 = state == true and M.colors.good or (state == false and M.colors.bad or M.colors.muted)
    end)
end

-- task.spawn directly preserves the executor/script capability context more reliably than
-- spawning a manually-created coroutine on some Roblox runtimes.
function M.spawn(callback)
    if M.dead then return end
    local thread
    thread = task.spawn(function()
        local ok, err = xpcall(callback, debug.traceback)
        M.workers[coroutine.running()] = nil
        if not ok and not M.dead then
            M.trace(err)
            M.status("An error occurred. Open Log for details.", false)
        end
    end)
    M.workers[thread] = true
    return thread
end

function M.resolve(root, path)
    local node = root
    for segment in path:gmatch("[^%.]+") do
        node = node and node:FindFirstChild(segment)
        if not node then return nil end
    end
    return node
end

M.confidenceRank = {Candidate = 1, Strong = 2, Confirmed = 3}
M.noisyNames = {
    ["tool"] = true, ["tools"] = true, ["toolscript"] = true, ["toolgiver"] = true,
    ["toolgiverclickpart"] = true, ["handle"] = true, ["template"] = true,
    ["clear tools"] = true, ["cleartools"] = true, ["item"] = true, ["items"] = true,
    ["button"] = true, ["icon"] = true, ["frame"] = true, ["model"] = true
}

function M.cleanCandidateName(value)
    if type(value) ~= "string" then return nil end
    value = value:gsub("^%s+", ""):gsub("%s+$", "")
    if value == "" or #value > 120 then return nil end
    if M.noisyNames[value:lower()] then return nil end
    if value:match("^rbxassetid://") or value:match("^https?://") then return nil end
    return value
end

function M.addPath(entry, path)
    if type(path) ~= "string" or path == "" then return end
    entry.observedPaths = entry.observedPaths or {}
    if #entry.observedPaths < 12 and not table.find(entry.observedPaths, path) then
        entry.observedPaths[#entry.observedPaths + 1] = path
    end
end

function M.addSource(entry, source)
    if type(source) ~= "string" or source == "" then return end
    entry.sources = entry.sources or {}
    if #entry.sources < 12 and not table.find(entry.sources, source) then
        entry.sources[#entry.sources + 1] = source
    end
    entry.source = table.concat(entry.sources, " + ")
end

function M.discover(name, options)
    name = M.cleanCandidateName(name)
    if not name then return nil end
    options = options or {}
    local entry = M.observed[name]
    if not entry then
        entry = {
            name = name, displayName = options.displayName or name, category = options.category or "",
            kind = options.kind or "Deep discovery", icon = M.icon(options.icon), order = options.order or 1000000,
            source = "", sources = {}, catalog = false, filters = options.filters or "",
            observedPaths = {}, interactions = {}, observedCount = 0, confidence = options.confidence or "Candidate",
            route = options.route or "PickingTools", gunSkin = options.gunSkin, routeHint = options.routeHint,
            evidence = options.evidence
        }
        M.observed[name] = entry
    end
    entry.observedCount = (entry.observedCount or 0) + 1
    if options.displayName and (entry.displayName == entry.name or entry.displayName == "") then entry.displayName = options.displayName end
    if options.icon and (not entry.icon or entry.icon == "") then entry.icon = M.icon(options.icon) end
    if options.kind and (entry.kind == "Deep discovery" or options.confidence == "Confirmed") then entry.kind = options.kind end
    if options.route then entry.route = options.route end
    if options.gunSkin and not entry.gunSkin then entry.gunSkin = options.gunSkin end
    if options.routeHint and not entry.routeHint then entry.routeHint = options.routeHint end
    if options.evidence and not entry.evidence then entry.evidence = options.evidence end
    local currentRank = M.confidenceRank[entry.confidence] or 0
    local nextRank = M.confidenceRank[options.confidence] or 0
    if nextRank > currentRank then entry.confidence = options.confidence end
    M.addSource(entry, options.source or "Deep discovery")
    M.addPath(entry, options.path)
    if options.interaction then
        entry.interactions = entry.interactions or {}
        if #entry.interactions < 8 and not table.find(entry.interactions, options.interaction) then entry.interactions[#entry.interactions + 1] = options.interaction end
    end
    return entry
end

function M.seedPlaceDiscoveries()
    for _, name in ipairs({"CreditCardBoy", "CreditCardGirl"}) do
        M.discover(name, {kind = "Direct PickingTools", source = "Place script direct request", confidence = "Confirmed", route = "PickingTools", evidence = "Player8Handler invokes PickingTools with this button name."})
    end

    local serialized = {
        {"Large Pizza Pie Shared) (Full)", "Workspace / pizza tool giver"}, {"JettsTray", "Workspace / Jetts"},
        {"BananaIceCreamLarge", "Workspace / Jetts"}, {"JettsFruitPunch", "Workspace / Jetts"},
        {"MangoFuegoSauceBottle", "Workspace / Jetts"}, {"MinionsVoicebox", "Workspace / house tool room"},
        {"FlagGun", "Workspace / house tool room"}, {"Bananapeel", "Workspace / house tool room"},
        {"FartGun", "Workspace / house tool room"}, {"Moo Box", "Workspace / house tool room"}
    }
    for _, pair in ipairs(serialized) do
        M.discover(pair[1], {kind = "Serialized Tool", source = "Place.rbxl Tool instance", confidence = "Confirmed", path = pair[2]})
    end

    -- Strong world-giver names recovered from the supplied place hierarchy. These include
    -- ToolGivers, FoodTools and explicit ToolGiverClickPart/ToolGiver interactions that may
    -- never stream to the client during an ordinary scan.
    local worldGiverSeeds = {
        "Apple", "Assault", "Axe", "BabyBottle", "BabyHippo", "BabyMonkey", "Banana", "BankGateKeyWhite",
        "Basketball", "BBQTool", "Bloxaide", "BlueKeycard", "Bomb", "Book", "BottledWater", "Brush",
        "ChipsGreen", "ChipsRed", "ChocolateBar", "Coke", "FireModel", "FireX", "FishingRod", "GlockBrown",
        "Hairbrush", "Hammer", "HotDog", "Icecream", "IcecreamBubblegum", "IcecreamCake", "IcecreamChocolate",
        "IcecreamMint", "IcecreamVanilla", "JettsTray", "Milk", "Minions2026_BananaPeel",
        "Minions2026_DynamitePlunger", "Minions2026_JamesCamera", "Money", "Newspaper", "Phone", "Pizza",
        "POIFlyer", "Popcorn", "Present", "ShakeChocolate", "Shotgun", "SignBlack", "SignPink", "SignRed",
        "SoccerBall", "Summer2026Photostrip", "TakeOutPizza", "Toothbrush", "Trophy", "Vacuum", "Waffle",
        "Weights", "Wrench"
    }
    for _, name in ipairs(worldGiverSeeds) do
        M.discover(name, {kind = "Place world-giver", source = "Place.rbxl tool-giver hierarchy", confidence = "Strong", routeHint = "World interaction / server validated"})
    end

    local toolsContainerSeeds = {
        "Bobcat Chew Toy", "CatWand", "Dynamite", "Jett’s Chew Toy", "Medicaldropper", "MedicalGloves",
        "MedicalSpray", "Medicine", "Plate", "SmallBall", "TennisBall"
    }
    for _, name in ipairs(toolsContainerSeeds) do
        M.discover(name, {kind = "Place Tools-container entry", source = "Place.rbxl Tools container", confidence = "Strong", routeHint = "World interaction / server validated"})
    end

    for _, name in ipairs({
        "JungleNuggets4", "JungleNuggets8", "JungleNuggets12",
        "BananaIceCreamSmall", "BananaIceCreamMedium", "BananaIceCreamLarge",
        "JettsFruitPunch", "MangoFuegoSauceBottle"
    }) do
        M.discover(name, {kind = "Jetts tool", source = "JettsMenuConstants", confidence = "Strong", routeHint = "SubmitJettsOrder (world/order context required)"})
    end

    local foods = {"Brownies", "Cake", "Chicken", "Cookies", "Enchiladas", "Lasagne", "Muffins", "Pizza"}
    for _, food in ipairs(foods) do
        M.discover("BakeFull_" .. food, {kind = "Baking tool", source = "BakingConfig generated name", confidence = "Confirmed", routeHint = "BakeOven / GrabBaked context required"})
        M.discover("BakePortion_" .. food, {kind = "Baking tool", source = "BakingConfig generated name", confidence = "Confirmed", routeHint = "BakeOven / GrabBaked context required"})
    end
    M.discover("Burnt Food", {kind = "Baking tool", source = "BakingConfig.BURNT_FOOD_TOOL", confidence = "Confirmed", routeHint = "BakeOven / GrabBaked context required"})

    for _, name in ipairs({
        "JulyFireworkBasic", "JulyFireworkBobcat", "JulyFireworkBrookhaven",
        "JulyFireworkFloorSparkler", "JulyFireworkGalaxySpiral", "JulyFireworkHeartBurst"
    }) do
        M.discover(name, {kind = "Firework tool", source = "FireworkConstants", confidence = "Confirmed", routeHint = "Firework system; paid types use server inventory"})
    end

    for _, name in ipairs({"Axe", "Hammer", "Wrench"}) do
        M.discover(name, {kind = "Known tool", source = "MetalDetectorToolsUtil", confidence = "Strong", evidence = "Explicit prohibited-tool name in shared client code."})
    end

    -- They are merged/deduplicated with ToolsConfig and the place-derived discoveries above.
    local lightningHubKnown = {
        "BabyRattle", "Baseball", "Basketball", "Boombox", "Bow", "Box", "Candle", "CandyCane",
        "Couch", "Crystal", "DSLRCamera", "Donut", "EggLauncher", "FireHose", "Firework", "FireX",
        "Football", "GhostDetector", "Glider", "GlowingBatons", "GuardTorch", "HorseBrush", "JoustBlue",
        "JoustRed", "Lipstick", "Megaphone", "PaintRoller", "PaperbagFire", "PropMaker", "ScoreCard",
        "Slingshot", "Snowball", "SoccerBall", "SprayCan", "Thermos", "WaterBalloon", "WateringCan",
        "WhackABunnyMallet", "YogaMat"
    }
    for _, name in ipairs(lightningHubKnown) do
        M.discover(name, {kind = "LightningHub known tool", source = "LightningHub V48 full catalog fallback", confidence = "Strong"})
    end
end

function M.scanToolFields(root, source, basePath, budget)
    if type(root) ~= "table" then return 0, 0 end
    local seen, found, visited = {}, 0, 0
    local maxNodes = budget or 90000
    local directKeys = {
        tool = true, toolname = true, toolid = true, fulltool = true, portiontool = true,
        rewardtool = true, grantedtool = true, equiptool = true, backpacktool = true
    }
    local function safeKeyText(key)
        local ok, text = pcall(tostring, key)
        return ok and text or "<key>"
    end
    local function rawField(tbl, a, b)
        if type(tbl) ~= "table" then return nil end
        local v = rawget(tbl, a)
        if v == nil and b then v = rawget(tbl, b) end
        return v
    end
    local function walk(node, path, depth)
        if type(node) ~= "table" or seen[node] or depth > 14 or visited >= maxNodes then return end
        seen[node] = true

        -- Use next/rawget only. Some loaded libraries expose metatable-backed tables
        -- (for example sleitnick_signal) whose normal .Name/.Type access throws.
        -- Deep discovery must never execute arbitrary __index/__pairs behavior.
        local key, value = next(node)
        while key ~= nil do
            visited = visited + 1
            if visited >= maxNodes then return end
            local keyText = safeKeyText(key)
            local keyNorm = keyText:gsub("[^%w]", ""):lower()
            local childPath = path == "" and keyText or (path .. "/" .. keyText)
            if type(value) == "string" then
                local isToolKey = directKeys[keyNorm] or keyNorm:find("toolname", 1, true) or keyNorm:find("toolid", 1, true)
                if isToolKey then
                    if M.discover(value, {kind = "Remote/module reference", source = source, confidence = "Strong", path = childPath, evidence = "String stored in a tool-named field."}) then found = found + 1 end
                end
            elseif type(value) == "table" then
                local recordName = rawField(value, "Name", "name")
                local recordType = rawField(value, "Type", "type") or rawField(value, "Class", "class")
                if type(recordName) == "string" and type(recordType) == "string" and recordType:lower():find("tool", 1, true) then
                    if M.discover(recordName, {kind = "Remote/module tool record", source = source, confidence = "Strong", path = childPath, evidence = "Record type contains 'tool'."}) then found = found + 1 end
                end
                walk(value, childPath, depth + 1)
            end
            if visited % 700 == 0 then task.wait() end
            key, value = next(node, key)
        end
    end
    walk(root, basePath or "", 0)
    return found, visited
end

function M.mineStaticModules()
    local specs = {
        {path = "Modules.Shared.Housing.Baking.BakingConfig", label = "BakingConfig"},
        {path = "Modules.Shared.Fireworks.FireworkConstants", label = "FireworkConstants"},
        {path = "Modules.Shared.Game.JettsMenuConstants", label = "JettsMenuConstants"},
        {path = "Modules.Shared.Tools.MetalDetectorToolsUtil", label = "MetalDetectorToolsUtil"}
    }
    local total = 0
    for _, spec in ipairs(specs) do
        local module = M.resolve(M.RS, spec.path)
        if module and module:IsA("ModuleScript") then
            local ok, result = pcall(require, module)
            if ok and type(result) == "table" then
                local found = M.scanToolFields(result, "Loaded " .. spec.label, spec.path, 12000)
                total = total + (found or 0)
                if spec.label == "BakingConfig" and type(result.Foods) == "table" then
                    for food in pairs(result.Foods) do
                        M.discover("BakeFull_" .. tostring(food), {kind = "Baking tool", source = "Live BakingConfig", confidence = "Confirmed", routeHint = "BakeOven / GrabBaked context required"})
                        M.discover("BakePortion_" .. tostring(food), {kind = "Baking tool", source = "Live BakingConfig", confidence = "Confirmed", routeHint = "BakeOven / GrabBaked context required"})
                    end
                    if type(result.BURNT_FOOD_TOOL) == "string" then M.discover(result.BURNT_FOOD_TOOL, {kind = "Baking tool", source = "Live BakingConfig", confidence = "Confirmed"}) end
                elseif spec.label == "FireworkConstants" then
                    local map = result.TOOL_NAME_BY_TYPE
                    if type(map) == "table" then for _, name in pairs(map) do M.discover(name, {kind = "Firework tool", source = "Live FireworkConstants", confidence = "Confirmed"}) end end
                    if type(result.ALL_TYPE_NAMES) == "table" then
                        for _, typ in ipairs(result.ALL_TYPE_NAMES) do
                            M.discover("JulyFirework" .. tostring(typ), {kind = "Firework tool", source = "Live FireworkConstants", confidence = "Strong"})
                        end
                    end
                elseif spec.label == "JettsMenuConstants" and type(result.JETTS_MENU_ITEMS_ALL) == "table" then
                    for name, data in pairs(result.JETTS_MENU_ITEMS_ALL) do
                        local toolName = type(data) == "table" and data.toolName or name
                        M.discover(toolName, {kind = "Jetts tool", source = "Live JettsMenuConstants", confidence = "Confirmed", routeHint = "SubmitJettsOrder (world/order context required)", icon = type(data) == "table" and data.image or nil})
                    end
                elseif spec.label == "MetalDetectorToolsUtil" and type(result.ADDITIONAL_PROHIBITED_TOOLS) == "table" then
                    for _, name in ipairs(result.ADDITIONAL_PROHIBITED_TOOLS) do M.discover(name, {kind = "Known tool", source = "Live MetalDetectorToolsUtil", confidence = "Strong"}) end
                end
            else
                M.trace(spec.label .. " require skipped: " .. tostring(result))
            end
        end
    end

    return total
end

function M.mineLoadedModules()
    local getter = getloadedmodules or M.env.getloadedmodules
    if type(getter) ~= "function" then return 0 end
    local ok, modules = pcall(getter)
    if not ok or type(modules) ~= "table" then return 0 end
    local found, checked, skipped = 0, 0, 0
    for _, module in ipairs(modules) do
        local readable, isModule, moduleName, modulePath = pcall(function()
            return module and module:IsA("ModuleScript"), module and module.Name or "", module and module:GetFullName() or ""
        end)
        if readable and isModule then
            local lower = tostring(moduleName):lower()
            if lower:find("tool", 1, true) or lower:find("baking", 1, true) or lower:find("firework", 1, true) or lower:find("jett", 1, true) then
                checked += 1
                local success, result = pcall(require, module)
                if success and type(result) == "table" then
                    local n = M.scanToolFields(result, "Loaded module " .. tostring(moduleName), tostring(modulePath), 5000)
                    found += (n or 0)
                else
                    skipped += 1
                end
            end
        elseif not readable then
            skipped += 1
        end
        if checked >= 120 then break end
    end
    M.trace(string.format("Loaded-module mining checked %d likely modules, added %d field hits, skipped %d protected/unreadable modules.", checked, found, skipped))
    return found
end

function M.inferGiverCandidate(object)
    if not object then return nil, nil end
    local current = object
    local hops = 0
    while current and current ~= game and hops < 8 do
        local lower = current.Name:lower()
        if lower == "toolgiverclickpart" or lower == "toolgiver" then
            local parent = current.Parent
            if parent then return M.cleanCandidateName(parent.Name), parent:GetFullName() end
        end
        local parent = current.Parent
        if parent then
            local plower = parent.Name:lower()
            if plower == "toolgivers" or plower == "foodtools" then return M.cleanCandidateName(current.Name), current:GetFullName() end
            if plower == "tools" then
                local grand = parent.Parent
                if current:IsA("Model") then return M.cleanCandidateName(current.Name), current:GetFullName() end
                if grand and grand.Name and not M.noisyNames[grand.Name:lower()] then return M.cleanCandidateName(grand.Name), grand:GetFullName() end
            end
        end
        current, hops = parent, hops + 1
    end
    return nil, nil
end

function M.inspectDiscoveryObject(object)
    if not object then return 0 end
    local added = 0
    if object:IsA("Tool") then
        M.observe(object, "Observed live")
        return 1
    end

    if object:IsA("StringValue") then
        local lower = object.Name:gsub("[^%w]", ""):lower()
        if lower == "toolname" or lower == "toolid" or lower == "currenttool" then
            if M.discover(object.Value, {kind = "StringValue tool reference", source = "Live object metadata", confidence = "Strong", path = object:GetFullName()}) then added = added + 1 end
        end
    end

    if object:IsA("ClickDetector") or object:IsA("ProximityPrompt") then
        local name, path = M.inferGiverCandidate(object)
        if name and M.discover(name, {kind = "World tool giver", source = object.ClassName, confidence = "Strong", path = path, routeHint = "World interaction / server validated", interaction = object}) then added = added + 1 end
    end

    if object:IsA("GuiButton") then
        local path = object:GetFullName()
        local lowerPath = path:lower()
        local hasToolMarker = object:FindFirstChild("GreenCheckMark") ~= nil
        local specialMenu = lowerPath:find("bankcards", 1, true) or lowerPath:find("jettsordermenu", 1, true)
        if hasToolMarker or specialMenu then
            local name = M.cleanCandidateName(object.Name)
            local lowerName = name and name:lower() or ""
            local ignored = lowerName == "back" or lowerName == "close" or lowerName == "search" or lowerName == "buy" or lowerName == "clear"
            if name and not ignored and not lowerName:find("category", 1, true) then
                if M.discover(name, {kind = "Native tool UI", source = "Live tool-related UI", confidence = "Strong", path = path, route = "PickingTools"}) then added = added + 1 end
            end
        end
    end

    local lname = object.Name:lower()
    if lname:find("tool", 1, true) or lname:find("gun", 1, true) or lname:find("firework", 1, true) or lname:find("bake", 1, true) or lname:find("jett", 1, true) then
        local ok, attrs = pcall(object.GetAttributes, object)
        if ok and type(attrs) == "table" then
            for key, value in pairs(attrs) do
                local kn = tostring(key):gsub("[^%w]", ""):lower()
                if type(value) == "string" and (kn == "tool" or kn == "toolname" or kn == "toolid" or kn:find("toolname", 1, true)) then
                    if M.discover(value, {kind = "Attribute tool reference", source = "Live attributes", confidence = "Strong", path = object:GetFullName() .. " [" .. tostring(key) .. "]"}) then added = added + 1 end
                end
            end
        end
    end
    return added
end

-- For read-only discovery calls only. A timed-out inventory call is never retried.
function M.readWithTimeout(label, seconds, callback)
    local result
    local worker = M.spawn(function() result = table.pack(pcall(callback)) end)
    local started = os.clock()
    while not result and not M.dead and os.clock() - started < seconds do task.wait(0.05) end
    if not result then
        if worker then pcall(task.cancel, worker); M.workers[worker] = nil end
        M.trace(label .. ": timed out")
        return false, "Timed out"
    end
    if not result[1] then M.trace(label .. ": " .. tostring(result[2])) end
    return table.unpack(result, 1, result.n)
end

function M.inventory()
    local owned, holdable = {}, {}
    local roots = {M.Player:FindFirstChildOfClass("Backpack"), M.Player.Character, M.Player:FindFirstChild("StarterGear")}
    for index = 1, 3 do
        local root = roots[index]
        if root then
            for _, tool in ipairs(root:GetChildren()) do
                if tool:IsA("Tool") then
                    owned[tool.Name] = tool
                    if index < 3 then holdable[tool.Name] = tool end
                    local gun = tool:FindFirstChild("GunName")
                    if gun and gun:IsA("StringValue") and gun.Value ~= "" then
                        owned[gun.Value] = tool
                        if index < 3 then holdable[gun.Value] = tool end
                    end
                end
            end
        end
    end
    return owned, holdable
end

function M.hold(name)
    local _, available = M.inventory()
    local tool = available[name]
    local character = M.Player.Character
    local humanoid = character and character:FindFirstChildOfClass("Humanoid")
    if not tool or not humanoid or humanoid.Health <= 0 then
        M.status("No usable copy of " .. name .. " is in your backpack yet.", false)
        return false
    end
    if tool.Parent ~= character then humanoid:EquipTool(tool) end
    M.status("Holding " .. name, true)
    return true
end

-- Brookhaven's stock client-to-client tool flow recovered from the supplied place:
-- sender:    PlayerTrigEvent:FireServer("ToolGiveToServer", recipientPlayer, toolName)
-- recipient: PlayerTrigEvent:FireServer("AcceptedToolToServer", toolName, senderPlayer)
-- The handoff probe below deliberately exercises only the recipient half, using a real
-- other Player as the donor identity. This lets us test the same route used by menus
-- that emulate an incoming player gift without relying on PickingTools.
function M.playerTrigRemote()
    local re = M.RS:FindFirstChild("RE")
    local remote = re and re:FindFirstChild("1Playe1rTrigge1rEven1t")
    if remote and remote:IsA("RemoteEvent") then return remote end
    return nil
end

function M.otherPlayers()
    local list = {}
    for _, player in ipairs(M.Players:GetPlayers()) do
        if player ~= M.Player then list[#list + 1] = player end
    end
    table.sort(list, function(a, b) return a.Name:lower() < b.Name:lower() end)
    return list
end

function M.resolveDonor()
    local list = M.otherPlayers()
    if #list == 0 then
        M.handoffDonorName = nil
        return nil
    end
    if M.handoffDonorName then
        for _, player in ipairs(list) do
            if player.Name == M.handoffDonorName then return player end
        end
    end
    M.handoffDonorName = list[1].Name
    return list[1]
end

function M.refreshDonorButton()
    if not M.ui or not M.ui.donor then return end
    local donor = M.resolveDonor()
    if donor then
        M.ui.donor.Text = "Donor: " .. donor.DisplayName .. "  @" .. donor.Name .. "   [cycle]"
        M.ui.donor.TextColor3 = M.colors.text
    else
        M.ui.donor.Text = "Donor: no other player in server"
        M.ui.donor.TextColor3 = M.colors.bad
    end
end

function M.cycleDonor(step)
    local list = M.otherPlayers()
    if #list == 0 then
        M.handoffDonorName = nil
        M.refreshDonorButton()
        M.status("Player handoff needs another player in the server.", false)
        return nil
    end
    local index = 0
    for i, player in ipairs(list) do
        if player.Name == M.handoffDonorName then index = i break end
    end
    index = index + (step or 1)
    if index > #list then index = 1 elseif index < 1 then index = #list end
    M.handoffDonorName = list[index].Name
    M.refreshDonorButton()
    M.details()
    M.status("Handoff donor set to " .. list[index].Name, true)
    return list[index]
end

function M.handoff(entry, intent)
    if M.dead or not entry or entry.isCategory then return false end
    if entry.kind == "Gun skin" then
        M.status("Gun-skin rows use GunEquip, not the player handoff Tool route.", false)
        return false
    end
    if M.busy then
        M.status("Waiting for the current request to finish.")
        return false
    end
    local remote = M.playerTrigRemote()
    if not remote then
        entry.lastResult = "HANDOFF REMOTE MISSING"
        M.status('RE["1Playe1rTrigge1rEven1t"] was not found.', false)
        M.render(false)
        return false
    end
    local donor = M.resolveDonor()
    if not donor then
        entry.lastResult = "NO DONOR"
        M.status("Join with Client2 / another player, then use Handoff receive.", false)
        M.render(false)
        return false
    end

    local before = {}
    local ownedBefore = M.inventory()
    for name, tool in pairs(ownedBefore) do before[tool] = name end

    local operation = {name = entry.name, intent = intent or "get", started = os.clock(), route = "PlayerHandoff"}
    M.busy = operation
    M.nextRequest = os.clock() + 0.35
    entry.lastResult = "HANDOFF..."
    M.trace("Handoff accept: item=" .. entry.name .. " donor=" .. donor.Name .. " via AcceptedToolToServer")
    M.status("Accepting " .. entry.name .. " as if offered by " .. donor.Name .. "...")
    M.render(false)

    M.spawn(function()
        local ok, err = pcall(function()
            remote:FireServer("AcceptedToolToServer", entry.name, donor)
        end)
        if not ok then
            if M.busy == operation then M.busy = nil end
            entry.lastResult = "HANDOFF ERROR"
            M.trace("Handoff error " .. entry.name .. ": " .. tostring(err))
            M.status("Handoff call failed: " .. tostring(err):sub(1, 160), false)
            M.render(false)
            return
        end

        local deadline = os.clock() + 3.25
        local exact, newTool, newName
        repeat
            local owned = M.inventory()
            exact = owned[entry.name]
            if exact then break end
            for name, tool in pairs(owned) do
                if not before[tool] then newTool, newName = tool, name break end
            end
            if newTool then break end
            task.wait(0.08)
        until M.dead or os.clock() >= deadline
        if M.dead then return end
        if M.busy == operation then M.busy = nil end
        M.nextRequest = os.clock() + 0.15

        if exact then
            entry.lastResult = "HANDOFF SPAWNED"
            M.status("Handoff received " .. entry.name .. " from donor route.", true)
            if intent == "hold" then M.hold(entry.name) end
        elseif newTool then
            entry.lastResult = "HANDOFF -> " .. tostring(newName)
            M.status("Handoff created a Tool named " .. tostring(newName) .. " (requested " .. entry.name .. ").", true)
            M.trace("Handoff alias: requested=" .. entry.name .. " observed=" .. tostring(newName))
            if intent == "hold" then M.hold(newName) end
        else
            entry.lastResult = "HANDOFF SENT"
            M.status("AcceptedToolToServer was sent, but no new Tool was observed. Try another donor/item and check Log.", false)
        end
        M.render(false)
    end)
    return true
end

function M.useWorldInteraction(entry, intent)
    local interaction
    for _, candidate in ipairs(entry.interactions or {}) do
        if candidate and candidate.Parent then interaction = candidate break end
    end
    if not interaction then
        M.status("The native giver is no longer loaded. Deep scan again near the giver.", false)
        return false
    end
    local ok, err
    if interaction:IsA("ClickDetector") then
        local fire = fireclickdetector or M.env.fireclickdetector
        if type(fire) ~= "function" then M.status("ClickDetector found, but this runtime cannot fire it.", false); return false end
        ok, err = pcall(fire, interaction)
    elseif interaction:IsA("ProximityPrompt") then
        local fire = fireproximityprompt or M.env.fireproximityprompt
        if type(fire) ~= "function" then M.status("ProximityPrompt found, but this runtime cannot fire it.", false); return false end
        ok, err = pcall(fire, interaction)
    else
        M.status("Unsupported world interaction: " .. interaction.ClassName, false)
        return false
    end
    if not ok then M.status("World interaction failed: " .. tostring(err):sub(1, 160), false); return false end
    M.status("Triggered native giver for " .. entry.name .. ". Waiting for inventory...")
    M.spawn(function()
        local deadline = os.clock() + 3
        local found
        repeat
            task.wait(0.12)
            found = M.inventory()[entry.name]
        until found or M.dead or os.clock() >= deadline
        if M.dead then return end
        if found then
            entry.lastResult = "SPAWNED"
            if intent == "hold" then M.hold(entry.name) else M.status("Received " .. entry.name .. " from world interaction.", true) end
        else
            entry.lastResult = "GIVER SENT"
            M.status("Giver triggered, but no matching Tool reached your inventory. The server may require proximity/state or use a different final tool name.", false)
        end
        M.render(false)
    end)
    return true
end

function M.act(entry, intent)
    if M.dead or not entry then return end
    local owned = M.inventory()
    local decision = M.actionDecision(entry, intent, owned[entry.name] ~= nil, M.busy ~= nil)
    if decision == "category" then
        M.category, M.filter, M.page = entry.name, "All tools", 1
        M.render(true)
        return
    elseif decision == "hold" then M.hold(entry.name); return
    elseif decision == "owned" then M.status("Already in your inventory: " .. entry.name, true); return
    elseif decision == "absent" then M.status("This tool is not in your inventory."); return
    elseif decision == "busy" then M.status("Waiting for the current item request to finish."); return
    elseif decision == "interaction" then M.useWorldInteraction(entry, intent); return
    elseif decision == "unavailable" then
        M.status("No usable native request path was found for " .. entry.name, false)
        return
    end

    if os.clock() < M.nextRequest then M.status("Give the last request a moment to settle."); return end
    local path, args = M.requestArgs(entry)
    local remote = path and M.resolve(M.RS, path)
    if not remote or not remote:IsA("RemoteFunction") then
        if entry.interactions and #entry.interactions > 0 and (intent == "get" or intent == "hold") then
            M.useWorldInteraction(entry, intent)
            return
        end
        M.status("Native request path is unavailable: " .. tostring(path), false)
        entry.lastResult = "ROUTE MISSING"
        M.render(false)
        return
    end

    local operation = {name = entry.name, intent = intent, started = os.clock()}
    M.busy = operation
    M.nextRequest = os.clock() + 0.45
    M.status((intent == "remove" and "Removing " or "Requesting ") .. entry.name .. "...")
    M.trace("Request " .. intent .. ": " .. entry.name .. " via " .. path)
    M.details()

    M.spawn(function()
        local ok, response = pcall(function() return remote:InvokeServer(table.unpack(args)) end)
        if M.dead then return end
        M.trace("Response " .. entry.name .. ": " .. (ok and tostring(response) or ("error: " .. tostring(response))))

        local deadline = os.clock() + 2.2
        local found = M.inventory()[entry.name]
        while os.clock() < deadline and not M.dead do
            found = M.inventory()[entry.name]
            if (intent == "remove" and not found) or (intent ~= "remove" and found) then break end
            if not ok then break end
            task.wait(0.08)
        end
        if M.dead then return end
        M.busy = nil
        M.nextRequest = os.clock() + 0.18

        if intent == "remove" and not found then
            entry.lastResult = "REMOVED"
            M.status("Tool is no longer in your inventory: " .. entry.name, true)
        elseif intent ~= "remove" and found then
            entry.lastResult = "SPAWNED"
            if intent == "hold" then M.hold(entry.name) else M.status("Received " .. entry.name, true) end
        elseif not ok then
            entry.lastResult = "ERROR"
            -- If a real native world giver/prompt is loaded, try that as the second route.
            if entry.interactions and #entry.interactions > 0 and (intent == "get" or intent == "hold") then
                M.status("Direct request errored; trying loaded native giver for " .. entry.name .. "...")
                M.render(false)
                task.wait(0.05)
                M.useWorldInteraction(entry, intent)
                return
            end
            M.status("Request failed: " .. tostring(response):sub(1, 160), false)
        elseif response == false then
            entry.lastResult = "REJECTED"
            if entry.interactions and #entry.interactions > 0 and (intent == "get" or intent == "hold") then
                M.status("PickingTools rejected; trying loaded native giver for " .. entry.name .. "...")
                M.render(false)
                task.wait(0.05)
                M.useWorldInteraction(entry, intent)
                return
            end
            M.status(entry.name .. " was rejected/locked by the server.", false)
        elseif intent ~= "remove" then
            if response ~= true and entry.interactions and #entry.interactions > 0 then
                entry.lastResult = "TRYING GIVER"
                M.status("Direct request returned no tool; trying loaded native giver for " .. entry.name .. "...")
                M.render(false)
                task.wait(0.05)
                M.useWorldInteraction(entry, intent)
                return
            end
            entry.lastResult = response == true and "ACCEPTED" or "SENT"
            M.status("Request sent for " .. entry.name .. "; exact Tool object was not observed yet.", response == true and true or nil)
        else
            entry.lastResult = "UNCHANGED"
            M.status("Tool is still present. Server response: " .. tostring(response), false)
        end
        M.render(false)
    end)
end

function M.merge()
    local previous = M.byName or {}
    local merged = {}
    local function cloneEntry(entry)
        local copy = {}
        for key, value in pairs(entry) do
            if type(value) == "table" and (key == "observedPaths" or key == "sources" or key == "interactions") then
                local t = {}
                for i, item in ipairs(value) do t[i] = item end
                copy[key] = t
            else copy[key] = value end
        end
        return copy
    end

    -- Native config wins for display/category/gun metadata.
    for name, entry in pairs(M.catalog) do
        local copy = cloneEntry(entry)
        if previous[name] then copy.lastResult = previous[name].lastResult end
        copy.sources = {entry.source}
        copy.route = copy.route or (not copy.isCategory and "PickingTools" or nil)
        merged[name] = copy
    end

    -- Add every other mined name. Every non-category name is actionable: ordinary extras
    -- default to PickingTools and can additionally carry native giver interactions.
    for name, observation in pairs(M.observed) do
        if merged[name] then
            local target = merged[name]
            for _, path in ipairs(observation.observedPaths or {}) do M.addPath(target, path) end
            for _, source in ipairs(observation.sources or {}) do M.addSource(target, source) end
            if observation.interactions then
                target.interactions = target.interactions or {}
                for _, interaction in ipairs(observation.interactions) do
                    if #target.interactions < 8 and not table.find(target.interactions, interaction) then target.interactions[#target.interactions + 1] = interaction end
                end
            end
            if observation.evidence and not target.evidence then target.evidence = observation.evidence end
        else
            local copy = cloneEntry(observation)
            if previous[name] then copy.lastResult = previous[name].lastResult end
            copy.route = copy.route or "PickingTools"
            copy.kind = copy.kind or "Extra mined tool"
            copy.category = copy.category or ""
            merged[name] = copy
        end
    end

    M.entries = {}
    for _, entry in pairs(merged) do
        entry.categoryPath = M.categoryPath(entry, M.categories)
        M.entries[#M.entries + 1] = entry
    end
    table.sort(M.entries, function(a, b)
        -- Normalize optional booleans before comparing. nil and false must be treated
        -- identically or table.sort can receive a non-transitive comparator.
        local aCategory = a.isCategory == true
        local bCategory = b.isCategory == true
        if aCategory ~= bCategory then return not aCategory end

        local aCatalog = a.catalog == true
        local bCatalog = b.catalog == true
        if aCatalog ~= bCatalog then return aCatalog end

        local aOrder = tonumber(a.order) or 0
        local bOrder = tonumber(b.order) or 0
        if aOrder ~= bOrder then return aOrder < bOrder end

        local aName = tostring(a.name or ""):lower()
        local bName = tostring(b.name or ""):lower()
        if aName ~= bName then return aName < bName end

        -- Equal sort keys are equal; never claim both directions are ordered.
        return false
    end)
    M.byName = merged
    local okRender, renderErr = pcall(M.render, false)
    if not okRender then M.trace("Render skipped: " .. tostring(renderErr)) end
end

function M.observe(tool, source)
    if not tool:IsA("Tool") or tool.Name == "" then return end
    local entry = M.discover(tool.Name, {displayName = tool.Name, kind = "Observed Tool", icon = tool.TextureId,
        source = source or "Observed live", confidence = "Confirmed", path = tool:GetFullName()})
    if entry then
        local gun = tool:FindFirstChild("GunName")
        if gun and gun:IsA("StringValue") and gun.Value ~= "" then
            M.discover(gun.Value, {displayName = gun.Value, kind = "Observed gun skin", source = source or "Observed live",
                confidence = "Confirmed", path = tool:GetFullName() .. ".GunName", routeHint = "Gun skin currently represented by this Tool instance"})
        end
    end
end

function M.scanWorld()
    if M.scanning or M.dead then return end
    M.scanning = true
    M.ui.scan.Text = "Deep scanning..."
    M.status("Deep-scanning replicated services, tool-givers, UI metadata, tags, and loaded tool modules...")
    M.spawn(function()
        local ok, err = pcall(function()
            local checked, found = 0, 0
            local roots = {M.RS, workspace, M.Players, game:GetService("StarterPack"), game:GetService("StarterGui"), M.PlayerGui}
            local seenRoots = {}
            for _, root in ipairs(roots) do
                if root and not seenRoots[root] then
                    seenRoots[root] = true
                    found = found + M.inspectDiscoveryObject(root)
                    for _, object in ipairs(root:GetDescendants()) do
                        if M.dead then return end
                        checked = checked + 1
                        found = found + M.inspectDiscoveryObject(object)
                        if checked % 350 == 0 then task.wait() end
                    end
                end
            end

            local tagsOk, tags = pcall(function() return M.Tags:GetAllTags() end)
            if tagsOk and type(tags) == "table" then
                for _, tag in ipairs(tags) do
                    local lower = tostring(tag):lower()
                    if lower:find("tool", 1, true) or lower:find("gun", 1, true) or lower:find("weapon", 1, true) or lower:find("firework", 1, true) then
                        for _, object in ipairs(M.Tags:GetTagged(tag)) do
                            if object:IsA("Tool") then M.observe(object, "Tag: " .. tag); found = found + 1
                            else
                                local ancestor = object:FindFirstAncestorOfClass("Tool")
                                if ancestor then M.observe(ancestor, "Tag: " .. tag); found = found + 1 end
                            end
                        end
                    end
                end
            end

            found = found + (M.mineStaticModules() or 0)
            found = found + (M.mineLoadedModules() or 0)
            M.deepStats.lastChecked, M.deepStats.lastHits = checked, found
            M.trace(string.format("Deep scan checked %d replicated objects and produced %d discovery hits. Streaming still limits world-only content.", checked, found))
        end)
        if M.dead then return end
        M.scanning = false
        M.ui.scan.Text = "Deep scan"
        if not ok then M.trace("Deep scan: " .. tostring(err)) end
        M.merge()
        if not M.mining and not M.busy then M.status("Deep scan finished. All newly mined names were added to the actionable spawn union.", ok) end
    end)
end

function M.nativeFallback()
    local result, seenRoots = {}, {}
    local ignore = {Template=true, ClearTools=true, BackButton=true, Delete=true, GreenCheckMark=true, Search=true, Close=true}
    local function addButton(button, source)
        if not button:IsA("GuiButton") or ignore[button.Name] then return end
        local name = M.cleanCandidateName(button.Name)
        if not name then return end
        local path = button:GetFullName()
        local looksTool = button:FindFirstChild("GreenCheckMark") ~= nil or path:lower():find("tool", 1, true)
            or path:lower():find("bankcards", 1, true) or path:lower():find("jetts", 1, true)
        if not looksTool then return end
        local icon = button:FindFirstChild("Icon")
        local image = icon and (icon:IsA("ImageLabel") or icon:IsA("ImageButton")) and icon.Image or ""
        result[name] = result[name] or {name=name, displayName=name, category="", icon=image, order=button.LayoutOrder,
            kind="Native UI tool", source=source, sources={source}, catalog=false, filters="", observedPaths={path}, route="PickingTools"}
    end
    local function scanRoot(root, source)
        if not root or seenRoots[root] then return end
        seenRoots[root] = true
        for _, obj in ipairs(root:GetDescendants()) do
            if obj:IsA("GuiButton") then addButton(obj, source) end
        end
    end

    for _, root in ipairs(M.Tags:GetTagged("ToolsMenu")) do scanRoot(root, "Tagged native ToolsMenu") end
    for _, root in ipairs(M.Tags:GetTagged("LoadableToolEntriesFilter")) do scanRoot(root, "Tagged loadable tool menu") end
    local starterGui = game:GetService("StarterGui")
    for _, root in ipairs({M.PlayerGui, starterGui}) do
        if root then
            for _, obj in ipairs(root:GetDescendants()) do
                if obj.Name == "ToolsMenu" or obj.Name == "MainToolMenuFilter" then scanRoot(obj, "Native tool UI") end
            end
        end
    end
    return result
end

function M.getCatalogFast(generation)
    local module = M.resolve(M.RS, "Modules.Shared.DB.Tools.ToolsConfig")
    local db
    if module and module:IsA("ModuleScript") then
        local ok, result = pcall(require, module)
        if ok and type(result) == "table" then
            db = result
            if type(db.cache) == "table" then return db.cache, "ToolsConfig.cache", nil end
        end
    end

    -- Direct public RemoteConfig is fast in this place and also gives us the full package
    -- tree for extra tool-name mining. Do it before waiting on GetConfig.
    local remote = M.RS:FindFirstChild("GameSdkRemoteConfigFunction")
    if remote and remote:IsA("RemoteFunction") then
        local success, packages, defaultPackage = M.readWithTimeout("RemoteConfig", 15, function() return remote:InvokeServer() end)
        if success and type(packages) == "table" then
            M.remoteTree = packages
            local config, packageName = M.findConfig(packages, defaultPackage)
            if type(config) == "table" then return config, "RemoteConfig / " .. tostring(packageName), packages end
        end
    end
    if M.dead or M.generation ~= generation then return nil, nil, nil, "cancelled" end

    -- Kick the game's own loader, then wait a short bounded period for cache.
    if module and db then
        pcall(function()
            local controllerModule = M.resolve(M.RS, "Modules.Client.Databases.DatabaseRemoteConfigController")
            if controllerModule and controllerModule:IsA("ModuleScript") then
                local controller = require(controllerModule)
                if type(controller) == "table" and type(controller.LoadDatabase) == "function" then controller.LoadDatabase(module) end
            end
        end)
        local deadline = os.clock() + 12
        while os.clock() < deadline and not M.dead and M.generation == generation do
            if type(db.cache) == "table" then return db.cache, "ToolsConfig.cache (after native load)", M.remoteTree end
            task.wait(0.12)
        end
        if type(db.GetConfig) == "function" then
            local success, config = M.readWithTimeout("ToolsConfig.GetConfig", 8, function() return db.GetConfig() end)
            if success and type(config) == "table" then return config, "ToolsConfig.GetConfig()", M.remoteTree end
        end
    end
    return nil, nil, M.remoteTree, "complete ToolsConfig unavailable"
end

function M.mine()
    if M.mining or M.dead then return end
    M.mining = true
    M.generation += 1
    local generation = M.generation
    M.ui.refresh.Text = "Mining..."
    M.status("Building the full spawn union...")
    M.spawn(function()
        local ok, errorMessage = xpcall(function()
            -- Start with place/menu-derived names immediately; they remain deduplicated.
            M.observed = {}
            M.seedPlaceDiscoveries()

            local config, source, packages, configError = M.getCatalogFast(generation)
            if M.dead or M.generation ~= generation then return end
            if config then
                local entries, categories, stats = M.parseConfig(config, source)
                M.catalog, M.categories, M.rawConfig, M.stats = entries, categories, config, stats
                M.source = source
                M.trace(string.format("ToolsConfig: %d tools / %d categories / %d raw rows", stats.tools, stats.categories, stats.rows))
            else
                M.catalog, M.categories, M.rawConfig = {}, {}, nil
                M.source = "Extra discovery union only"
                M.trace("ToolsConfig unavailable: " .. tostring(configError))
            end

            if type(packages) == "table" then
                local deepHits, visited = M.scanToolFields(packages, "RemoteConfig full tree", "", 160000)
                M.trace(string.format("Remote-tree tool-field scan: %d hits across %d nodes", deepHits or 0, visited or 0))
            end

            M.mineStaticModules()
            M.mineLoadedModules()

            -- Merge every currently rendered native UI tool too.
            local uiRows, uiCount = M.nativeFallback(), 0
            for name, entry in pairs(uiRows) do
                uiCount += 1
                M.discover(name, {displayName=entry.displayName, kind=entry.kind, icon=entry.icon, source=entry.source,
                    confidence="Strong", route="PickingTools", path=entry.observedPaths and entry.observedPaths[1]})
            end
            M.trace("Native UI extra rows observed: " .. tostring(uiCount))

            M.merge()
            local nativeCount, extraCount = 0, 0
            for _, entry in ipairs(M.entries) do
                if not entry.isCategory then
                    if entry.catalog then nativeCount += 1 else extraCount += 1 end
                end
            end
            M.coverage = string.format("Spawn union: %d native + %d extra = %d tools", nativeCount, extraCount, nativeCount + extraCount)
            M.status(string.format("Loaded %d actionable tool names. Deep scan can add streamed/tool-giver names.", nativeCount + extraCount), true)
        end, debug.traceback)

        if M.dead then return end
        M.mining = false
        if not ok then
            M.trace("Mining stage error: " .. tostring(errorMessage))
            M.status("Mining stage hit a protected object/module; continuing with the catalog already recovered.", false)
        end
        if M.ui and M.ui.refresh then pcall(function() M.ui.refresh.Text = "Re-mine all" end) end
        M.scanWorld()
    end)
end

function M.export(mode)
    if mode == "log" then return table.concat(M.logs, "\n") end
    local entries = M.entries
    if mode == "names" then
        local lines = {}
        for _, e in ipairs(entries) do if not e.isCategory then lines[#lines + 1] = e.name end end
        return table.concat(lines, "\n")
    end
    if mode == "json" then
        local items = {}
        for _, e in ipairs(entries) do
            items[#items + 1] = {name=e.name, displayName=e.displayName, kind=e.kind, category=e.category,
                categoryPath=e.categoryPath, source=e.source, route=e.route, gunSkin=e.gunSkin, item=e.item,
                filters=e.filters, requirement=e.requirement, hiddenUnlessUnlocked=e.hiddenUnlessUnlocked,
                sources=e.sources, evidence=e.evidence, observedPaths=e.observedPaths, lastResult=e.lastResult, raw=e.raw}
        end
        local ok, json = pcall(function()
            return M.Http:JSONEncode({minerVersion=M.version, placeId=game.PlaceId, source=M.source, coverage=M.coverage,
                exportedAtUtc=os.date("!%Y-%m-%dT%H:%M:%SZ"), entries=items})
        end)
        return ok and json or ("JSON export failed: " .. tostring(json))
    end
    local lines = {"Tool Miner " .. M.version, "Source: " .. M.source, "Coverage: " .. M.coverage,
        "Every non-category row has an actionable native request route; server unlock checks still apply.", "",
        "Name\tKind\tCategory\tDisplay name\tRoute\tLast result\tSource\tItem / requirement"}
    local function clean(s) return tostring(s or ""):gsub("[\t\r\n]", " ") end
    for _, e in ipairs(entries) do
        lines[#lines + 1] = table.concat({clean(e.name), clean(e.kind), clean(e.categoryPath), clean(e.displayName),
            clean(e.route or ""), clean(e.lastResult or "UNTESTED"), clean(e.source), clean((e.item or "") .. " " .. (e.requirement or ""))}, "\t")
    end
    return table.concat(lines, "\n")
end

function M.new(class, parent, properties)
    local object = Instance.new(class)
    for key, value in pairs(properties or {}) do object[key] = value end
    object.Parent = parent
    return object
end

function M.round(object, radius)
    M.new("UICorner", object, {CornerRadius = UDim.new(0, radius or 6)})
end

function M.label(parent, text, pos, size, textSize, color)
    return M.new("TextLabel", parent, {BackgroundTransparency = 1, Text = text, Position = pos, Size = size,
        Font = Enum.Font.Gotham, TextSize = textSize or 12, TextColor3 = color or M.colors.text,
        TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd})
end

function M.button(parent, text, pos, size, callback, bucket)
    local button = M.new("TextButton", parent, {Text = text, Position = pos, Size = size, BorderSizePixel = 0,
        BackgroundColor3 = M.colors.row, TextColor3 = M.colors.text, Font = Enum.Font.GothamMedium,
        TextSize = 12, AutoButtonColor = true})
    M.round(button)
    if callback then M.track(button.Activated:Connect(callback), bucket) end
    return button
end

function M.showExport(mode)
    M.modalMode = mode
    M.ui.exportTitle.Text = mode == "log" and "Tool Miner log" or "Export all discoveries"
    M.ui.exportBox.Text = M.export(mode)
    M.ui.modal.Visible = true
    for name, button in pairs(M.ui.exportModes) do button.BackgroundColor3 = name == mode and M.colors.active or M.colors.row end
end

function M.copy()
    local copier = setclipboard or M.env.setclipboard
    if type(copier) == "function" then
        local ok = pcall(copier, M.ui.exportBox.Text)
        if ok then M.ui.copy.Text = "Copied"; return end
    end
    M.ui.exportBox:CaptureFocus()
    M.ui.exportBox.CursorPosition = #M.ui.exportBox.Text + 1
    M.ui.exportBox.SelectionStart = 1
    M.ui.copy.Text = "Press Ctrl+C"
end

function M.details()
    if not M.ui or M.dead then return end
    local entry = M.selected and M.byName and M.byName[M.selected]
    if not entry then
        M.ui.detailTitle.Text = "Select a tool"
        M.ui.detailImage.Image = ""
        M.ui.detailText.Text = "v5 adds Brookhaven's real player-to-player tool handoff route from the supplied place.\n\nPick a donor (Client2 works), select any normal tool, then use Handoff receive. This sends the same recipient acceptance action the stock client uses after another player offers a tool."
        M.ui.get.Visible, M.ui.hold.Visible, M.ui.handoff.Visible, M.ui.remove.Visible = false, false, false, false
        return
    end
    local owned, usable = M.inventory()
    local state = owned[entry.name] and (usable[entry.name] and "In inventory" or "In StarterGear") or "Not in inventory"
    local lines = {entry.name, "", "Type: " .. tostring(entry.kind or "Tool"),
        "Category: " .. (entry.categoryPath or "General"), "Source: " .. tostring(entry.source or "Unknown"),
        "Inventory: " .. state, "Route: " .. tostring(entry.route or "PickingTools"),
        "Last request: " .. tostring(entry.lastResult or "UNTESTED"),
        "Handoff donor: " .. tostring((M.resolveDonor() and M.resolveDonor().Name) or "none")}
    if entry.gunSkin then lines[#lines + 1] = "Base gun: " .. tostring(entry.gunSkin) end
    if entry.item then lines[#lines + 1] = "Linked item: " .. tostring(entry.item) end
    if entry.filters and entry.filters ~= "" then lines[#lines + 1] = "Menu filters: " .. entry.filters end
    if entry.requirement and entry.requirement ~= "" then lines[#lines + 1] = "Requirement: " .. entry.requirement end
    if entry.hiddenUnlessUnlocked then lines[#lines + 1] = "Native menu hides this until unlocked." end
    if entry.evidence then lines[#lines + 1] = "Evidence: " .. entry.evidence end
    if entry.sources and #entry.sources > 1 then
        lines[#lines + 1] = "\nFound by:"
        for _, source in ipairs(entry.sources) do lines[#lines + 1] = "- " .. source end
    end
    if entry.observedPaths and #entry.observedPaths > 0 then
        lines[#lines + 1] = "\nObserved paths:"
        for _, path in ipairs(entry.observedPaths) do lines[#lines + 1] = path end
    end
    if entry.interactions and #entry.interactions > 0 then
        lines[#lines + 1] = "\nLoaded native giver/prompt fallback: YES"
    end
    M.ui.detailTitle.Text = entry.displayName or entry.name
    M.ui.detailImage.Image = entry.icon or ""
    M.ui.detailText.Text = table.concat(lines, "\n")
    M.ui.get.Visible = true
    M.ui.hold.Visible = not entry.isCategory
    M.ui.handoff.Visible = not entry.isCategory and entry.kind ~= "Gun skin"
    M.ui.remove.Visible = not entry.isCategory and owned[entry.name] ~= nil
    M.ui.get.Text = entry.isCategory and "Open category" or (owned[entry.name] and "Already owned" or "Get tool")
    if M.busy and M.busy.name == entry.name then M.ui.get.Text = "Pending..." end
end

function M.render(resetScroll)
    if not M.ui or M.dead then return end
    local owned, matched = M.inventory(), {}
    local nativeCount, extraCount, categoryCount, testedCount = 0, 0, 0, 0
    for _, entry in ipairs(M.entries) do
        if entry.isCategory then categoryCount += 1
        elseif entry.catalog then nativeCount += 1
        else extraCount += 1 end
        if not entry.isCategory and entry.lastResult then testedCount += 1 end

        local include = not entry.isCategory
        if M.filter == "Native config" then include = entry.catalog and not entry.isCategory
        elseif M.filter == "Extra mined" then include = not entry.catalog and not entry.isCategory
        elseif M.filter == "Gun skins" then include = entry.kind == "Gun skin"
        elseif M.filter == "Categories" then include = entry.isCategory
        elseif M.filter == "In bag" then include = owned[entry.name] ~= nil end
        if include and M.inCategory(entry, M.category, M.categories) and M.matches(entry, M.query) then matched[#matched + 1] = entry end
    end
    M.filtered = matched
    local pages = math.max(1, math.ceil(#matched / M.pageSize))
    M.page = math.clamp(M.page, 1, pages)
    M.ui.subtitle.Text = string.format("%d tools total  /  %d native  /  %d extra mined  /  %d tested  |  %s", nativeCount + extraCount, nativeCount, extraCount, testedCount, M.coverage)
    M.ui.category.Text = M.category ~= "" and ("Category: " .. M.category .. "  [clear]") or "Category: All  /  select Categories to browse"
    M.ui.pages.Text = string.format("%d results  |  %d / %d", #matched, M.page, pages)
    for name, button in pairs(M.ui.tabs) do button.BackgroundColor3 = name == M.filter and M.colors.active or M.colors.row end
    M.disconnect(M.rows)
    for _, child in ipairs(M.ui.list:GetChildren()) do if child:IsA("GuiObject") then child:Destroy() end end
    local first = (M.page - 1) * M.pageSize + 1
    for index = first, math.min(#matched, first + M.pageSize - 1) do
        local entry = matched[index]
        local row = M.button(M.ui.list, "", UDim2.fromOffset(0, 0), UDim2.new(1, -8, 0, 61), function()
            M.selected = entry.name
            M.render(false)
        end, M.rows)
        row.LayoutOrder = index
        row.BackgroundColor3 = M.selected == entry.name and M.colors.active or M.colors.row
        M.new("ImageLabel", row, {BackgroundTransparency=1, Size=UDim2.fromOffset(45,45), Position=UDim2.fromOffset(8,8), Image=entry.icon or "", ScaleType=Enum.ScaleType.Fit})
        local title = M.label(row, entry.displayName or entry.name, UDim2.fromOffset(64,9), UDim2.new(1,-138,0,20), 13)
        title.Font = Enum.Font.GothamMedium
        M.label(row, entry.name .. "  /  " .. (entry.categoryPath or entry.kind or "Tool"), UDim2.fromOffset(64,33), UDim2.new(1,-80,0,16), 10, M.colors.muted)
        local badge
        if owned[entry.name] then badge = "IN BAG"
        elseif entry.isCategory then badge = "FOLDER"
        elseif entry.lastResult then badge = entry.lastResult
        elseif entry.catalog then badge = "NATIVE"
        else badge = "EXTRA" end
        M.label(row, badge, UDim2.new(1,-86,0,12), UDim2.fromOffset(80,18), 9, owned[entry.name] and M.colors.good or M.colors.accent)
    end
    M.ui.empty.Visible = #matched == 0
    M.ui.empty.Text = #M.entries == 0 and "Mining the complete tool union...\nOpen Log if loading fails." or "No results for this filter."
    if resetScroll then M.ui.list.CanvasPosition = Vector2.new(0, 0) end
    M.details()
end

function M.unload()
    if M.dead then return end
    M.dead = true
    M.generation = M.generation + 1
    M.disconnect(M.connections)
    M.disconnect(M.rows)
    local current = coroutine.running()
    for thread in pairs(M.workers) do if thread ~= current then pcall(task.cancel, thread) end end
    table.clear(M.workers)
    if M.gui then M.gui:Destroy() end
    if M.env.ToolMinerSpawnerUnload == M.unload then M.env.ToolMinerSpawnerUnload = nil end
end

function M.buildUI()
    M.gui = M.new("ScreenGui", nil, {Name = "ToolMinerSpawner", ResetOnSpawn = false,
        IgnoreGuiInset = true, DisplayOrder = 100, ZIndexBehavior = Enum.ZIndexBehavior.Sibling})
    -- Keep the UI under PlayerGui. Some runtimes let the startup thread parent into
    -- gethui/CoreGui but later worker threads cannot touch those Instances and throw
    -- "lacking capability Plugin". PlayerGui avoids that capability split entirely.
    local parented = pcall(function() M.gui.Parent = M.PlayerGui end)
    if not parented then error("Tool Miner could not create its PlayerGui interface.") end
    local U = {}
    M.ui = U
    U.main = M.new("Frame", M.gui, {Size = UDim2.fromOffset(920, 650), AnchorPoint = Vector2.new(0.5, 0.5),
        Position = UDim2.fromScale(0.5, 0.5), BackgroundColor3 = M.colors.bg, BorderSizePixel = 0})
    M.round(U.main, 10)
    M.new("UIStroke", U.main, {Color = M.colors.line, Thickness = 1})
    U.scale = M.new("UIScale", U.main, {Scale = 1})
    U.header = M.new("Frame", U.main, {Size = UDim2.new(1, 0, 0, 63), BackgroundTransparency = 1, Active = true})
    local title = M.label(U.header, "Tool Miner v5.1 - Player Handoff", UDim2.fromOffset(16, 10), UDim2.fromOffset(205, 24), 20)
    title.Font = Enum.Font.GothamBold
    U.subtitle = M.label(U.header, "Reading the live catalog...", UDim2.fromOffset(16, 39), UDim2.new(1, -30, 0, 18), 10, M.colors.muted)
    U.refresh = M.button(U.header, "Re-mine all", UDim2.fromOffset(426, 9), UDim2.fromOffset(83, 28), M.mine)
    U.scan = M.button(U.header, "Deep scan", UDim2.fromOffset(515, 9), UDim2.fromOffset(91, 28), M.scanWorld)
    M.button(U.header, "Export", UDim2.fromOffset(612, 9), UDim2.fromOffset(72, 28), function() M.showExport("report") end)
    M.button(U.header, "Log", UDim2.fromOffset(690, 9), UDim2.fromOffset(57, 28), function() M.showExport("log") end)
    M.button(U.header, "Hide", UDim2.fromOffset(753, 9), UDim2.fromOffset(60, 28), function() U.main.Visible = false end)
    M.button(U.header, "X", UDim2.fromOffset(866, 9), UDim2.fromOffset(36, 28), M.unload)
    U.search = M.new("TextBox", U.main, {Position = UDim2.fromOffset(16, 72), Size = UDim2.new(1, -32, 0, 34),
        BackgroundColor3 = M.colors.panel, BorderSizePixel = 0, Text = "", PlaceholderText = "Search every mined tool name, source, category, event item...",
        PlaceholderColor3 = M.colors.muted, TextColor3 = M.colors.text, Font = Enum.Font.Gotham, TextSize = 13,
        ClearTextOnFocus = false, TextXAlignment = Enum.TextXAlignment.Left})
    M.round(U.search)
    M.new("UIPadding", U.search, {PaddingLeft = UDim.new(0, 10), PaddingRight = UDim.new(0, 10)})
    U.tabs = {}
    for index, name in ipairs({"All tools", "Native config", "Extra mined", "Gun skins", "Categories", "In bag"}) do
        U.tabs[name] = M.button(U.main, name, UDim2.fromOffset(16 + (index - 1) * 148, 116), UDim2.fromOffset(140, 29), function()
            M.filter, M.page = name, 1
            M.render(true)
        end)
    end
    U.category = M.button(U.main, "Category: All", UDim2.fromOffset(16, 153), UDim2.fromOffset(554, 25), function()
        M.category, M.page = "", 1
        M.render(true)
    end)
    U.list = M.new("ScrollingFrame", U.main, {Position = UDim2.fromOffset(16, 187), Size = UDim2.fromOffset(554, 385),
        BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 4, ScrollBarImageColor3 = M.colors.accent,
        CanvasSize = UDim2.fromOffset(0, 0), AutomaticCanvasSize = Enum.AutomaticSize.Y})
    M.new("UIListLayout", U.list, {Padding = UDim.new(0, 5), SortOrder = Enum.SortOrder.LayoutOrder})
    U.empty = M.label(U.main, "", UDim2.fromOffset(42, 255), UDim2.fromOffset(480, 80), 13, M.colors.muted)
    U.empty.TextWrapped = true
    U.pages = M.label(U.main, "", UDim2.fromOffset(17, 584), UDim2.fromOffset(300, 24), 11, M.colors.muted)
    M.button(U.main, "Previous", UDim2.fromOffset(383, 580), UDim2.fromOffset(88, 29), function()
        M.page = M.page - 1; M.render(true)
    end)
    M.button(U.main, "Next", UDim2.fromOffset(479, 580), UDim2.fromOffset(88, 29), function()
        M.page = M.page + 1; M.render(true)
    end)
    U.detail = M.new("Frame", U.main, {Position = UDim2.fromOffset(585, 153), Size = UDim2.fromOffset(319, 456),
        BackgroundColor3 = M.colors.panel, BorderSizePixel = 0})
    M.round(U.detail)
    U.detailImage = M.new("ImageLabel", U.detail, {Position = UDim2.fromOffset(13, 12), Size = UDim2.fromOffset(52, 52),
        BackgroundTransparency = 1, Image = "", ScaleType = Enum.ScaleType.Fit})
    U.detailTitle = M.label(U.detail, "Select a tool", UDim2.fromOffset(76, 16), UDim2.fromOffset(229, 40), 15)
    U.detailTitle.TextWrapped = true
    U.detailTitle.Font = Enum.Font.GothamBold
    U.donor = M.button(U.detail, "Donor: scanning...", UDim2.fromOffset(13, 72), UDim2.fromOffset(292, 25), function()
        M.cycleDonor(1)
    end)
    U.donor.TextSize = 10
    U.donor.TextXAlignment = Enum.TextXAlignment.Left
    M.new("UIPadding", U.donor, {PaddingLeft = UDim.new(0, 8), PaddingRight = UDim.new(0, 8)})
    local scroll = M.new("ScrollingFrame", U.detail, {Position = UDim2.fromOffset(13, 105), Size = UDim2.fromOffset(295, 255),
        BackgroundTransparency = 1, BorderSizePixel = 0, CanvasSize = UDim2.fromOffset(0, 0),
        AutomaticCanvasSize = Enum.AutomaticSize.Y, ScrollBarThickness = 3})
    U.detailText = M.label(scroll, "", UDim2.fromOffset(0, 0), UDim2.new(1, -9, 0, 0), 12, M.colors.muted)
    U.detailText.AutomaticSize = Enum.AutomaticSize.Y
    U.detailText.TextWrapped = true
    U.detailText.TextYAlignment = Enum.TextYAlignment.Top
    U.detailText.TextTruncate = Enum.TextTruncate.None
    U.get = M.button(U.detail, "Get tool", UDim2.fromOffset(13, 377), UDim2.fromOffset(142, 30), function()
        M.act(M.byName and M.byName[M.selected], "get")
    end)
    U.get.BackgroundColor3 = M.colors.active
    U.hold = M.button(U.detail, "Hold", UDim2.fromOffset(163, 377), UDim2.fromOffset(142, 30), function()
        M.act(M.byName and M.byName[M.selected], "hold")
    end)
    U.handoff = M.button(U.detail, "Handoff receive", UDim2.fromOffset(13, 414), UDim2.fromOffset(142, 28), function()
        M.handoff(M.byName and M.byName[M.selected], "get")
    end)
    U.handoff.BackgroundColor3 = M.colors.active
    U.remove = M.button(U.detail, "Remove", UDim2.fromOffset(163, 414), UDim2.fromOffset(142, 28), function()
        M.act(M.byName and M.byName[M.selected], "remove")
    end)
    U.remove.TextColor3 = M.colors.bad
    U.status = M.label(U.main, "", UDim2.fromOffset(16, 617), UDim2.new(1, -32, 0, 23), 11, M.colors.muted)

    U.modal = M.new("Frame", U.main, {Size = UDim2.fromScale(1, 1), BackgroundColor3 = M.colors.bg,
        BorderSizePixel = 0, Visible = false, ZIndex = 20, Active = true})
    M.round(U.modal, 10)
    U.exportTitle = M.label(U.modal, "Export all discoveries", UDim2.fromOffset(16, 13), UDim2.fromOffset(600, 30), 18)
    M.button(U.modal, "Back", UDim2.new(1, -98, 0, 13), UDim2.fromOffset(80, 30), function()
        U.modal.Visible = false; M.modalMode = nil
    end)
    U.exportModes = {}
    for index, item in ipairs({{"names", "Names"}, {"report", "Full report"}, {"json", "JSON + raw config"}, {"log", "Log"}}) do
        U.exportModes[item[1]] = M.button(U.modal, item[2], UDim2.fromOffset(16 + (index - 1) * 170, 56), UDim2.fromOffset(161, 30), function()
            U.copy.Text = "Copy"; M.showExport(item[1])
        end)
    end
    U.copy = M.button(U.modal, "Copy", UDim2.new(1, -123, 0, 56), UDim2.fromOffset(107, 30), M.copy)
    local exportScroll = M.new("ScrollingFrame", U.modal, {Position = UDim2.fromOffset(16, 99), Size = UDim2.new(1, -32, 1, -139),
        BackgroundColor3 = M.colors.panel, BorderSizePixel = 0, CanvasSize = UDim2.fromOffset(0, 0),
        AutomaticCanvasSize = Enum.AutomaticSize.Y, ScrollBarThickness = 5})
    U.exportBox = M.new("TextBox", exportScroll, {Position = UDim2.fromOffset(8, 8), Size = UDim2.new(1, -21, 0, 32),
        AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, Text = "", MultiLine = true,
        ClearTextOnFocus = false, TextEditable = false, TextWrapped = true, Font = Enum.Font.Code, TextSize = 12,
        TextColor3 = M.colors.text, TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Top})
    M.label(U.modal, "Exports include the full spawn union and each tool's last request result. Server unlock checks still apply.", UDim2.fromOffset(16, 617), UDim2.new(1, -32, 0, 22), 11, M.colors.muted)

    M.track(U.search:GetPropertyChangedSignal("Text"):Connect(function()
        M.query, M.page = U.search.Text, 1
        M.render(true)
    end))
    M.track(M.Input.InputBegan:Connect(function(input, processed)
        if input.KeyCode == Enum.KeyCode.RightShift and not processed and not M.Input:GetFocusedTextBox() then
            U.main.Visible = not U.main.Visible
        end
    end))
    local drag
    M.track(U.header.InputBegan:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
            drag = {input = input, start = input.Position, position = U.main.Position}
        end
    end))
    M.track(M.Input.InputChanged:Connect(function(input)
        if drag and (input.UserInputType == Enum.UserInputType.MouseMovement or input == drag.input) then
            local delta = input.Position - drag.start
            U.main.Position = UDim2.new(drag.position.X.Scale, drag.position.X.Offset + delta.X,
                drag.position.Y.Scale, drag.position.Y.Offset + delta.Y)
        end
    end))
    M.track(M.Input.InputEnded:Connect(function(input)
        if drag and (input == drag.input or input.UserInputType == Enum.UserInputType.MouseButton1) then drag = nil end
    end))
    local function resize()
        local camera = workspace.CurrentCamera
        if camera then U.scale.Scale = math.clamp(math.min((camera.ViewportSize.X - 24) / 920, (camera.ViewportSize.Y - 24) / 650), 0.25, 1) end
    end
    local cameraConnection
    local function bindCamera()
        if cameraConnection then cameraConnection:Disconnect() end
        if workspace.CurrentCamera then cameraConnection = M.track(workspace.CurrentCamera:GetPropertyChangedSignal("ViewportSize"):Connect(resize)) end
        resize()
    end
    M.track(workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(bindCamera))
    bindCamera()
end

M.buildUI()
M.env.ToolMinerSpawnerUnload = M.unload
M.refreshDonorButton()

-- Log real incoming tool offers so the exact stock payload can be compared with the handoff probe.
do
    local handoffRemote = M.playerTrigRemote()
    if handoffRemote then
        M.track(handoffRemote.OnClientEvent:Connect(function(action, donor, icon, toolName)
            if action == "ToolGiveToClient" then
                M.lastIncomingOffer = {donor = donor, icon = icon, toolName = toolName, at = os.clock()}
                local donorName = typeof(donor) == "Instance" and donor.Name or tostring(donor)
                M.trace("Incoming native tool offer: donor=" .. donorName .. " tool=" .. tostring(toolName) .. " icon=" .. tostring(icon))
                if typeof(donor) == "Instance" and donor:IsA("Player") then
                    M.handoffDonorName = donor.Name
                    M.refreshDonorButton()
                end
            end
        end))
    else
        M.trace("Player handoff remote missing at startup: RE.1Playe1rTrigge1rEven1t")
    end
end

M.track(M.Players.PlayerAdded:Connect(function() task.defer(M.refreshDonorButton) end))
M.track(M.Players.PlayerRemoving:Connect(function(player)
    if M.handoffDonorName == player.Name then M.handoffDonorName = nil end
    task.defer(M.refreshDonorButton)
end))

M.render(true)
M.trace("Tool Miner v" .. M.version .. " started. PlaceId=" .. tostring(game.PlaceId))
M.trace("Capability fix active: PlayerGui-only UI + direct task.spawn workers + protected-module skips.")
M.trace("Deep-scan fix active: raw table traversal avoids metatable Signal traps; stable normalized sorter enabled.")
M.trace("Spawn Union layers: ToolsConfig + RemoteConfig tree + native UI + visible Tool instances + ToolGivers + dynamic configs + place-derived families + LightningHub-known names.")
M.trace("Player handoff route: AcceptedToolToServer(toolName, donorPlayer) via RE.1Playe1rTrigge1rEven1t.")
M.trace("Every normal tool can also be tested through Handoff receive using a real other player as donor identity.")

-- Inventory changes update visible state; one small poll also catches GunName changes.
M.spawn(function()
    local previous = ""
    while not M.dead do
        local owned = M.inventory()
        local names = {}
        for name in pairs(owned) do names[#names + 1] = name end
        table.sort(names)
        local signature = table.concat(names, "\n")
        if signature ~= previous then
            previous = signature
            for _, tool in pairs(owned) do M.observe(tool, "Your inventory") end
            M.merge()
        end
        task.wait(0.7)
    end
end)

-- Catch tools that stream/spawn after the initial deep scan without rescanning the whole place.
local function watchRoot(root)
    if not root then return end
    M.track(root.DescendantAdded:Connect(function(object)
        if M.dead then return end
        if object:IsA("Tool") then
            M.observe(object, "Spawned/streamed live")
            M.merge()
        elseif object:IsA("StringValue") then
            local lower = object.Name:gsub("[^%w]", ""):lower()
            if lower == "toolname" or lower == "toolid" then
                M.inspectDiscoveryObject(object)
                M.merge()
            end
        end
    end))
end
watchRoot(workspace)
watchRoot(M.RS)
watchRoot(M.PlayerGui)

M.mine()

Features & Information

this is an unfinished project its open source for a reason dont flame in comments lol if you dont like it finish it yourself..

Community Comments (0)

No comments yet for this script.

More Scripts for Brookhaven 🏡RP