local GlobalEnv = getgenv() local previousStop = GlobalEnv.__sabCarpetStop if type(previousStop) == "function" then pcall(previousStop, "Đã thay bằng phiên bản script mới") end local RunToken = {} GlobalEnv.__sabCarpetRunToken = RunToken local function cloneRef(instance) if typeof(cloneref) ~= "function" then return instance end local ok, result = pcall(cloneref, instance) return ok and result or instance end getgenv().CarpetFarmConfig = getgenv().CarpetFarmConfig or {} getgenv().RebirthConfig = getgenv().RebirthConfig or {} do local carpetConfig = getgenv().CarpetFarmConfig if carpetConfig.MinServerPlayers == nil then carpetConfig.MinServerPlayers = 3 end if carpetConfig.RichPlayerCash == nil then carpetConfig.RichPlayerCash = 20000000 end if carpetConfig.HobeoTrackEnabled == nil then carpetConfig.HobeoTrackEnabled = false end local config = getgenv().RebirthConfig if config.ChangeDelay == nil then config.ChangeDelay = 3 end if config.ChangeToFolderEnabled == nil then config.ChangeToFolderEnabled = true end if config.ChangeWithoutReplace == nil then config.ChangeWithoutReplace = false end if config.LockBase == nil then config.LockBase = false end config.From = config.From or "2fa6a21da9985f66f51a4ab0d4b61b18bb9f1cddccb7d1624a38350e5d6650dc" config.To = config.To or "20369b71b3a7dfc068b5622ef1d3127bdaf119c7e433606c38f08cfde1986d5c" end local CollectionService = cloneRef(game:GetService("CollectionService")) local PathfindingService = cloneRef(game:GetService("PathfindingService")) local Players = cloneRef(game:GetService("Players")) local ReplicatedStorage = cloneRef(game:GetService("ReplicatedStorage")) local Workspace = cloneRef(workspace) local Farm = { cashMultipliers = { K = 1e3, M = 1e6, B = 1e9, T = 1e12, Q = 1e15 }, CORE_LOOP_DELAY = 2, ACTION_POLL = 0.1, ACTION_VALIDATE_INTERVAL = 0.5, SNAPSHOT_POLL = 0.5, SNAPSHOT_CACHE_TTL = 5, CONTEST_SCAN_INTERVAL = 0.5, CAMP_SELL_THRESHOLD = 8, CLEANUP_TIMEOUT = 60, CLEANUP_RETRY = 30, DATA_SYNC_TIMEOUT = 60, CAMP_RECHECK = 2, ACTION_TIMEOUT = 35, RETURN_TIMEOUT = 10, LOCK_TIMEOUT = 6, LOCK_CONFIRM_TIMEOUT = 2, LOCK_RETRY = 2, CARPET_HEAD_INSET = 4, WAIT_FPS = 5, MOVE_FPS = 20, LOW_CASH_FOLDER_THRESHOLD = 50000, CHANGE_FOLDER_TIMEOUT = 20, CONTEST_SLICE = 1, CONTEST_RETRY = 0.25, RESERVATION_TIMEOUT = 120, TRANSIENT_RETRY = 5, SHOP_TIMEOUT = 25, SHOP_PROMPT_DELAY = 1.5, SHOP_BUY_DELAY = 2, SHOP_EQUIP_DELAY = 1, SPEED_COIL_NAME = "Speed Coil", player = nil, leaderstats = nil, cashValue = nil, cachedCash = nil, cashConnection = nil, movementCharacter = nil, movementHumanoid = nil, movementRoot = nil, movementConnections = {}, characterAddedConnection = nil, plotsFolder = nil, plotsFolderConnections = {}, plot = nil, watchedPlot = nil, cachedYourBase = nil, plotConnections = {}, synchronizer = nil, lastSynchronizerAttempt = 0, data = {}, cleanupActive = false, cleanupDeadline = 0, cleanupRetryAt = 0, dataSyncSince = nil, reservations = {}, promptRetryAt = {}, contestServed = {}, actionRetryAt = 0, lastLockAttempt = 0, speedCoilRetryAt = 0, speedCoilEquipRetryAt = 0, speedCoilDone = false, speedCoilUnknown = false, speedCoilDisabled = true, stoppedReason = nil, changingFolder = false, changedFolder = false, changeFolderTimedOut = false, promptCache = {}, promptCacheDirty = true, identityPromptCache = setmetatable({}, { __mode = "k" }), snapshotCache = nil, snapshotCacheDirty = true, watchedPodiums = nil, podiumConnections = {}, carpetCache = {}, carpetCacheDirty = true, watchedAnimals = {}, worldConnections = {}, statusLabel = nil, currentAction = "Đang khởi động", appliedFps = nil, campMode = false, campCheckedAt = 0, } local Action = {} if type(GlobalEnv.__sabCarpetTriedJobs) ~= "table" then -- ponytail: cache theo phiên executor; chỉ dùng file TTL nếu cần sống qua lúc đóng client. GlobalEnv.__sabCarpetTriedJobs = {} end local Fleet = { GuiService = cloneRef(game:GetService("GuiService")), HttpService = cloneRef(game:GetService("HttpService")), TeleportService = cloneRef(game:GetService("TeleportService")), API_BASE = "https://hobeojob.com/api", PLACE_ID = game.PlaceId, REQUEST_TIMEOUT = 30, REQUEST_TRIES = 3, REQUEST_RETRY_GAP = 2, PRESENCE_HEARTBEAT = 60, PRESENCE_STAGGER = 10, USERNAME_TRIES = 4, USERNAME_RETRY_GAP = 3, USERNAME_REQUEST_TIMEOUT = 60, USERNAME_CACHE_TTL = 24 * 60 * 60, USERNAME_REFRESH_INTERVAL = 800, FRIEND_CHECK_INTERVAL = 15, USERNAME_SETTLE = 0, USERNAME_STAGGER = 30, TELEPORT_WAIT = 6, TELEPORT_CONFIRM = 20, HOBEO_HOP_ATTEMPTS = 12, HOBEO_RETRY_GAP = 0.5, ROBLOX_PAGES = 6, MAX_ITEMS = 5000, started = false, checking = false, hopping = false, requestFn = nil, tried = GlobalEnv.__sabCarpetTriedJobs, jobReportQueue = {}, jobReportHead = 1, jobReportActive = false, currentJobId = nil, teleportFailed = false, teleportResult = nil, teleportStarted = false, unknownTeleport = false, teleportConnections = {}, friendCheckStarted = false, presencePostActive = false, nextPresenceAt = 0, registry = {}, registryReady = false, registryCount = 0, registryExpiresAt = 0, registryCacheAvailable = type(writefile) == "function" and type(readfile) == "function" and type(isfile) == "function", rng = Random.new(), } function Farm.isCurrentRun() return GlobalEnv.__sabCarpetRunToken == RunToken end GlobalEnv.__sabCarpetStop = function(reason) if not Farm.isCurrentRun() then return end Farm.stoppedReason = reason or "Script đã dừng" if type(Farm.cleanupRuntime) == "function" then pcall(Farm.cleanupRuntime) end end function Farm.updateStatusUi() local label = Farm.statusLabel if not label or not label.Parent then return end label.Text = "CARPET FARM\nĐang làm: " .. Farm.currentAction .. "\nCash: $" .. string.format("%.0f", Farm.getCash()) .. " | Players: " .. #Players:GetPlayers() end function Farm.setFps(cap) if Farm.appliedFps == cap or type(setfpscap) ~= "function" then return end if pcall(setfpscap, cap) then Farm.appliedFps = cap end end function Farm.markLoaded() local env = getgenv() env.__sabCarpetLoaded = true env.isloaded = true end function Farm.setStatus(action) if action then Farm.currentAction = action Farm.setFps(Farm.WAIT_FPS) end Farm.updateStatusUi() end function Farm.getStatusUiParent() if type(gethui) == "function" then local ok, parent = pcall(gethui) if ok and typeof(parent) == "Instance" then return parent end end local ok, coreGui = pcall(game.GetService, game, "CoreGui") if ok and coreGui then return cloneRef(coreGui) end return Farm.player and (Farm.player:FindFirstChild("PlayerGui") or Farm.player:WaitForChild("PlayerGui", 10)) end function Farm.installStatusUi() local uiParent = Farm.getStatusUiParent() local playerGui = Farm.player and Farm.player:FindFirstChild("PlayerGui") if not uiParent then return end local function clearOld(container) if not container then return end for _, child in ipairs(container:GetChildren()) do if child.Name == "CarpetFarmStatus" then child:Destroy() end end end clearOld(uiParent) if playerGui ~= uiParent then clearOld(playerGui) end local gui = Instance.new("ScreenGui") gui.Name = "CarpetFarmStatus" gui.ResetOnSpawn = false gui.IgnoreGuiInset = false gui.DisplayOrder = 999999 local parented = pcall(function() gui.Parent = uiParent end) if not parented then if not playerGui then gui:Destroy() return end gui.Parent = playerGui end local label = Instance.new("TextLabel") label.Name = "Status" label.AnchorPoint = Vector2.new(0.5, 0) label.Size = UDim2.new(0, 420, 0, 112) label.Position = UDim2.new(0.5, 0, 0, 18) label.BackgroundColor3 = Color3.fromRGB(18, 18, 24) label.BackgroundTransparency = 0.08 label.BorderSizePixel = 0 label.Font = Enum.Font.GothamBold label.TextColor3 = Color3.fromRGB(240, 242, 248) label.TextSize = 18 label.TextXAlignment = Enum.TextXAlignment.Left label.TextYAlignment = Enum.TextYAlignment.Center label.TextWrapped = true label.AutoLocalize = false label.Parent = gui local padding = Instance.new("UIPadding") padding.PaddingLeft = UDim.new(0, 16) padding.PaddingRight = UDim.new(0, 16) padding.Parent = label local corner = Instance.new("UICorner") corner.CornerRadius = UDim.new(0, 12) corner.Parent = label local stroke = Instance.new("UIStroke") stroke.Color = Color3.fromRGB(82, 88, 110) stroke.Thickness = 1 stroke.Parent = label Farm.statusLabel = label Farm.updateStatusUi() end Farm.items = { { name = "Gangster Footera", cost = 4000, required = true }, } function Farm.parseCash(value) local text = tostring(value):gsub(",", ""):gsub("%s", "") for suffix, multiplier in pairs(Farm.cashMultipliers) do local amount = tonumber(text:match("^([%d%.]+)" .. suffix .. "$")) if amount then return math.floor(amount * multiplier) end end return tonumber(text) or 0 end function Farm.bindCash(cashValue) if Farm.cashConnection then Farm.cashConnection:Disconnect() end Farm.cashConnection = nil Farm.cashValue = cashValue Farm.cachedCash = cashValue and Farm.parseCash(cashValue.Value) or nil if cashValue then Farm.cashConnection = cashValue:GetPropertyChangedSignal("Value"):Connect(function() Farm.cachedCash = Farm.parseCash(cashValue.Value) end) end end function Farm.getCash() local player = Farm.player if not player then return 0 end if not Farm.leaderstats or Farm.leaderstats.Parent ~= player then Farm.leaderstats = player:FindFirstChild("leaderstats") Farm.bindCash(Farm.leaderstats and Farm.leaderstats:FindFirstChild("Cash") or nil) end if not Farm.leaderstats then return 0 end if not Farm.cashValue or Farm.cashValue.Parent ~= Farm.leaderstats then Farm.bindCash(Farm.leaderstats:FindFirstChild("Cash")) end return Farm.cachedCash or 0 end function Farm.disconnectMovementSignals() for _, connection in ipairs(Farm.movementConnections) do connection:Disconnect() end Farm.movementConnections = {} end function Farm.bindMovementCharacter(character) Farm.disconnectMovementSignals() Farm.movementCharacter = character Farm.movementHumanoid = character and character:FindFirstChildOfClass("Humanoid") or nil Farm.movementRoot = character and character:FindFirstChild("HumanoidRootPart") or nil if character then Farm.movementConnections = { character.AncestryChanged:Connect(function(_, parent) if not parent and Farm.movementCharacter == character then Farm.bindMovementCharacter(nil) end end), } end end function Farm.bindMovementPlayer() if Farm.characterAddedConnection then Farm.characterAddedConnection:Disconnect() end Farm.characterAddedConnection = nil if not Farm.player then Farm.bindMovementCharacter(nil) return end Farm.characterAddedConnection = Farm.player.CharacterAdded:Connect(Farm.bindMovementCharacter) Farm.bindMovementCharacter(Farm.player.Character) end function Farm.getMovementRefs() local player = Farm.player if not player then return nil, nil, nil end if player.Character ~= Farm.movementCharacter then Farm.bindMovementCharacter(player.Character) end local character = Farm.movementCharacter if not character then return nil, nil, nil end if not Farm.movementHumanoid or Farm.movementHumanoid.Parent ~= character then Farm.movementHumanoid = character:FindFirstChildOfClass("Humanoid") end if not Farm.movementRoot or Farm.movementRoot.Parent ~= character then Farm.movementRoot = character:FindFirstChild("HumanoidRootPart") end if not Farm.movementHumanoid or not Farm.movementRoot or Farm.movementHumanoid.Health <= 0 then return nil, nil, nil end return character, Farm.movementHumanoid, Farm.movementRoot end function Farm.getSynchronizer() if Farm.synchronizer then return Farm.synchronizer end if tick() - Farm.lastSynchronizerAttempt < 5 then return nil end Farm.lastSynchronizerAttempt = tick() local packages = ReplicatedStorage:FindFirstChild("Packages") local module = packages and packages:FindFirstChild("Synchronizer") if not module then return nil end local ok, result = pcall(require, module) if ok and result then Farm.synchronizer = result return result end return nil end function Farm.getSyncedTable(index) if index == nil then return nil end local synchronizer = Farm.getSynchronizer() if not synchronizer or type(synchronizer.GetTableFromChannel) ~= "function" then return nil end local ok, result = pcall(synchronizer.GetTableFromChannel, synchronizer, index) return ok and type(result) == "table" and result or nil end function Farm.getAnimalList() local plot = Farm.findMyPlot() local baseData = plot and Farm.getSyncedTable(plot.Name) return baseData and type(baseData.AnimalList) == "table" and baseData.AnimalList or nil end function Farm.getDataModule(name) if Farm.data[name] then return Farm.data[name] end local datas = ReplicatedStorage:FindFirstChild("Datas") local module = datas and datas:FindFirstChild(name) if not module then return nil end local ok, result = pcall(require, module) if ok and result then Farm.data[name] = result return result end return nil end function Farm.getTool(name) local player = Farm.player local character = player and player.Character local tool = character and character:FindFirstChild(name) if tool and tool:IsA("Tool") then return tool end local backpack = player and player:FindFirstChildOfClass("Backpack") tool = backpack and backpack:FindFirstChild(name) return tool and tool:IsA("Tool") and tool or nil end function Farm.hasTool(name) return Farm.getTool(name) ~= nil end function Farm.speedCoilReady() if Farm.speedCoilDone or Farm.speedCoilUnknown or Farm.speedCoilDisabled or tick() < Farm.speedCoilRetryAt then return false end if Farm.hasTool(Farm.SPEED_COIL_NAME) then Farm.speedCoilDone = true return false end local shopItems = Farm.getDataModule("ShopItems") local item = shopItems and shopItems[Farm.SPEED_COIL_NAME] local price = item and tonumber(item.Price) local playerData = Farm.player and Farm.getSyncedTable(Farm.player) local coins = playerData and tonumber(playerData.Coins) if not price or price <= 0 or not coins then return false end return coins >= price, price end function Farm.getCoinsShopRefs() local shop = Workspace:FindFirstChild("Shop") local npc = shop and shop:FindFirstChild("ShopNPCCash") local prompt = npc and npc:FindFirstChild("OpenInterface") local target = npc and npc:FindFirstChild("Cube") if not npc or not npc:IsA("Model") or not target or not target:IsA("BasePart") or npc.PrimaryPart ~= target then return nil end if not prompt or not prompt:IsA("ProximityPrompt") or prompt.ActionText ~= "Coins Shop" then return nil end return npc, prompt, target end function Farm.getSpeedCoilUi() local playerGui = Farm.player and Farm.player:FindFirstChild("PlayerGui") local screen = playerGui and playerGui:FindFirstChild("CoinsShop") local panel = screen and screen:FindFirstChild("CoinsShop") local content = panel and panel:FindFirstChild("Content") local items = content and content:FindFirstChild("Items") local item = items and items:FindFirstChild(Farm.SPEED_COIL_NAME) local buy = item and item:FindFirstChild("Buy") local price = buy and buy:FindFirstChild("Price") local header = panel and panel:FindFirstChild("Header") local close = header and header:FindFirstChild("Close") if not screen or not screen:IsA("ScreenGui") or not panel or not panel:IsA("GuiObject") or not item or not item:IsA("GuiObject") or not buy or not buy:IsA("ImageButton") or not price or not price:IsA("TextLabel") or not close or not close:IsA("GuiButton") then return nil end return screen, panel, item, buy, price, close end function Farm.getGeneration(name, mutation, traits) local animals = Farm.getDataModule("Animals") local mutations = Farm.getDataModule("Mutations") local traitData = Farm.getDataModule("Traits") local animal = animals and name and animals[name] local base = animal and tonumber(animal.Generation) or 0 if base <= 0 then return nil end local hasMutation = mutation and mutation ~= "" local hasTraits = type(traits) == "table" and next(traits) ~= nil if (hasMutation and not mutations) or (hasTraits and not traitData) then return nil end local multiplier = 1 local sleepy = false if hasMutation and mutations[mutation] then multiplier += tonumber(mutations[mutation].Modifier) or 0 end if hasTraits then for _, traitName in pairs(traits) do local trait = traitData[traitName] if traitName == "Sleepy" then sleepy = true elseif trait then multiplier += tonumber(trait.MultiplierModifier) or 0 end end end return math.round(base * multiplier * (sleepy and 0.5 or 1)) end function Farm.disconnectPlotSignals() for _, connection in ipairs(Farm.plotConnections) do connection:Disconnect() end Farm.plotConnections = {} Farm.watchedPlot = nil Farm.cachedYourBase = nil end function Farm.markPlotDirty() Farm.plot = nil Farm.disconnectPlotSignals() Farm.markSnapshotDirty() end function Farm.getPlotsFolder() if Farm.plotsFolder and Farm.plotsFolder.Parent == Workspace then return Farm.plotsFolder end for _, connection in ipairs(Farm.plotsFolderConnections) do connection:Disconnect() end Farm.plotsFolderConnections = {} Farm.plotsFolder = Workspace:FindFirstChild("Plots") if Farm.plotsFolder then Farm.plotsFolderConnections = { Farm.plotsFolder.ChildAdded:Connect(Farm.markPlotDirty), Farm.plotsFolder.ChildRemoved:Connect(Farm.markPlotDirty), } end return Farm.plotsFolder end function Farm.getYourBase(plot) local sign = plot and plot:FindFirstChild("PlotSign") local yours = sign and sign:FindFirstChild("YourBase") return yours and yours:IsA("BillboardGui") and yours or nil end function Farm.watchMyPlot(plot, yourBase) if Farm.watchedPlot == plot and Farm.cachedYourBase == yourBase then return end Farm.disconnectPlotSignals() Farm.watchedPlot = plot Farm.cachedYourBase = yourBase Farm.plotConnections = { plot.AncestryChanged:Connect(function(_, parent) if not parent then Farm.markPlotDirty() end end), yourBase:GetPropertyChangedSignal("Enabled"):Connect(function() if not yourBase.Enabled then Farm.markPlotDirty() end end), } end function Farm.findMyPlot() local plots = Farm.getPlotsFolder() if not plots then return nil end if Farm.plot and Farm.plot.Parent == plots and Farm.cachedYourBase and Farm.cachedYourBase.Parent and Farm.cachedYourBase.Enabled then return Farm.plot end for _, plot in ipairs(plots:GetChildren()) do local yourBase = Farm.getYourBase(plot) if yourBase and yourBase.Enabled then Farm.plot = plot Farm.watchMyPlot(plot, yourBase) return plot end end Farm.markPlotDirty() return nil end function Farm.stopMovement() local _, humanoid, hrp = Farm.getMovementRefs() if humanoid and hrp then humanoid:MoveTo(hrp.Position) end end function Farm.setCampMode(plot, snapshot) Farm.campMode = snapshot and snapshot.plot == plot or false Farm.campCheckedAt = tick() return Farm.campMode end function Farm.farmReady(plot) if plot ~= Farm.findMyPlot() then return false end if tick() - Farm.campCheckedAt >= Farm.CAMP_RECHECK then Farm.setCampMode(plot, Farm.snapshot()) end return Farm.campMode end function Farm.shouldAbort(plot, deadline) return Farm.stoppedReason or tick() >= deadline or plot ~= Farm.findMyPlot() or not Farm.farmReady(plot) end function Action.shouldInterrupt(interruptCheck) if not interruptCheck then return false end local ok, interrupted = pcall(interruptCheck) return ok and interrupted == true end function Action.moveTo(position, deadline, plot, tolerance, expectedCharacter, interruptCheck) if Farm.shouldAbort(plot, deadline) or Action.shouldInterrupt(interruptCheck) then Farm.stopMovement() return false end local startCharacter, humanoid, hrp = Farm.getMovementRefs() if not startCharacter or (expectedCharacter and startCharacter ~= expectedCharacter) then return false end Farm.setFps(Farm.MOVE_FPS) local finished, reached = false, false local connection = humanoid.MoveToFinished:Connect(function(didReach) finished, reached = true, didReach end) humanoid:MoveTo(position) local moveDeadline = math.min(deadline, tick() + 15) while tick() < moveDeadline do task.wait(0.1) local characterNow, humanoidNow, hrpNow = Farm.getMovementRefs() if characterNow ~= startCharacter or humanoidNow ~= humanoid or not hrpNow then connection:Disconnect() return false end if Farm.shouldAbort(plot, deadline) or Action.shouldInterrupt(interruptCheck) then connection:Disconnect() humanoidNow:MoveTo(hrpNow.Position) return false end if (hrpNow.Position - position).Magnitude <= (tolerance or 4) then connection:Disconnect() return true end if finished then connection:Disconnect() return reached end end connection:Disconnect() Farm.stopMovement() return false end function Action.pathfindTo(position, deadline, plot, expectedCharacter, interruptCheck) if Farm.shouldAbort(plot, deadline) or Action.shouldInterrupt(interruptCheck) then Farm.stopMovement() return false end local startCharacter, _, hrp = Farm.getMovementRefs() if not hrp or (expectedCharacter and startCharacter ~= expectedCharacter) then return false end if (hrp.Position - position).Magnitude < 4 then return true end Farm.setFps(Farm.MOVE_FPS) local path = PathfindingService:CreatePath({ AgentRadius = 2, AgentHeight = 5, AgentCanJump = true }) local done, computed = false, false local computeThread = task.spawn(function() computed = pcall(function() path:ComputeAsync(hrp.Position, position) end) done = true end) while not done and tick() < deadline do if Farm.shouldAbort(plot, deadline) or Action.shouldInterrupt(interruptCheck) then pcall(task.cancel, computeThread) Farm.stopMovement() return false end task.wait(0.05) end if not done then pcall(task.cancel, computeThread) return false end if computed and path.Status == Enum.PathStatus.Success then for _, waypoint in ipairs(path:GetWaypoints()) do if Farm.shouldAbort(plot, deadline) or Action.shouldInterrupt(interruptCheck) then Farm.stopMovement() return false end local currentCharacter, humanoid, currentHrp = Farm.getMovementRefs() if not humanoid or not currentHrp or (expectedCharacter and currentCharacter ~= expectedCharacter) then Farm.stopMovement() return false end if waypoint.Action == Enum.PathWaypointAction.Jump then humanoid.Jump = true end if (currentHrp.Position - waypoint.Position).Magnitude >= 4 and not Action.moveTo( waypoint.Position, deadline, plot, nil, expectedCharacter, interruptCheck ) then return false end end return true end return Action.moveTo(position, deadline, plot, nil, expectedCharacter, interruptCheck) end function Farm.getLockState(plot) if not plot or not plot.Parent then return nil end local data = Farm.getSyncedTable(plot.Name) if data then return data.BlockEndTime ~= nil or data.BlockEndTimeFirstFloor ~= nil or data.BlockEndTimeSecondFloor ~= nil or data.BlockEndTimeThirdFloor ~= nil end local purchases = plot:FindFirstChild("Purchases") local plotBlock = purchases and purchases:FindFirstChild("PlotBlock") local main = plotBlock and plotBlock:FindFirstChild("Main") local gui = main and main:FindFirstChild("BillboardGui") local locked = gui and gui:FindFirstChild("Locked") local remaining = gui and gui:FindFirstChild("RemainingTime") if locked or remaining then return (locked and locked.Visible == true) or (remaining and remaining.Visible == true) end return nil end function Farm.getLockHitbox(plot) local purchases = plot and plot:FindFirstChild("Purchases") local plotBlock = purchases and purchases:FindFirstChild("PlotBlock") local hitbox = plotBlock and plotBlock:FindFirstChild("Hitbox") return hitbox and hitbox:IsA("BasePart") and hitbox or nil end function Action.lockBase(plot) local config = getgenv().RebirthConfig or {} if config.LockBase == false or Farm.getLockState(plot) == true then return true, nil end local hitbox = Farm.getLockHitbox(plot) if not hitbox then warn("Không tìm thấy PlotBlock.Hitbox để khóa base.") return false, nil end local candidate, nextContestCheck = nil, 0 local function contestReady() local now = tick() if now < nextContestCheck then return false end nextContestCheck = now + Farm.CONTEST_RETRY candidate = Farm.selectPrompt() if candidate then return true end return Farm.hasLiveContestPrompt(Farm.snapshot()) end local timeout = math.clamp(tonumber(config.LockMoveTimeoutSec) or Farm.LOCK_TIMEOUT, 3, Farm.ACTION_TIMEOUT) local deadline = tick() + timeout if not Action.pathfindTo(hitbox.Position, deadline, plot, nil, contestReady) then return false, candidate end if not Action.moveTo(hitbox.Position, deadline, plot, 1.5, nil, contestReady) then return false, candidate end local confirmDeadline = tick() + Farm.LOCK_CONFIRM_TIMEOUT while tick() < confirmDeadline do if contestReady() then Farm.stopMovement() return false, candidate end if Farm.getLockState(plot) == true then return true, nil end task.wait(0.1) end return false, candidate end function Farm.markSnapshotDirty() Farm.snapshotCacheDirty = true end function Farm.disconnectPodiumSignals() for _, connection in ipairs(Farm.podiumConnections) do connection:Disconnect() end Farm.podiumConnections = {} Farm.watchedPodiums = nil end function Farm.watchPodiums(podiums) if Farm.watchedPodiums == podiums then return end Farm.disconnectPodiumSignals() Farm.watchedPodiums = podiums Farm.identityPromptCache = setmetatable({}, { __mode = "k" }) Farm.markSnapshotDirty() if not podiums then return end Farm.podiumConnections = { podiums.ChildAdded:Connect(Farm.markSnapshotDirty), podiums.ChildRemoved:Connect(Farm.markSnapshotDirty), podiums.DescendantAdded:Connect(Farm.markSnapshotDirty), podiums.DescendantRemoving:Connect(Farm.markSnapshotDirty), } end function Farm.getIdentityPrompt(slot) if not slot then return nil end local cached = Farm.identityPromptCache[slot] if cached and cached.Parent and cached:IsDescendantOf(slot) then local action = tostring(cached.ActionText or "") if (action == "Grab" or action == "Open") and tostring(cached.ObjectText or "") ~= "" then return cached end end Farm.identityPromptCache[slot] = nil for _, object in ipairs(slot:GetDescendants()) do if object:IsA("ProximityPrompt") then local action = tostring(object.ActionText or "") if (action == "Grab" or action == "Open") and tostring(object.ObjectText or "") ~= "" then Farm.identityPromptCache[slot] = object return object end end end return nil end function Farm.snapshot() local now = tick() local plot = Farm.findMyPlot() local podiums = plot and plot:FindFirstChild("AnimalPodiums") Farm.watchPodiums(podiums) local cached = Farm.snapshotCache if not Farm.snapshotCacheDirty and cached and cached.plot == plot and cached.podiums == podiums and now - cached.refreshedAt < Farm.SNAPSHOT_CACHE_TTL then return cached end local animalList = Farm.getAnimalList() local snapshot = { plot = plot, podiums = podiums, slots = {}, income = 0, incomeKnown = animalList ~= nil, allResolved = true, refreshedAt = now, } for index = 1, 10 do local slot = podiums and podiums:FindFirstChild(tostring(index)) local synced = animalList and (animalList[index] or animalList[tostring(index)]) local identityPrompt = Farm.getIdentityPrompt(slot) local objectText = identityPrompt and tostring(identityPrompt.ObjectText or "") or "" local info = { index = index, slot = slot, state = "unresolved", objectText = objectText } if synced == "Empty" then info.state = "empty" elseif type(synced) == "table" and tostring(synced.Index or "") ~= "" then info.state = "occupied" info.syncedIndex = tostring(synced.Index) info.generation = Farm.getGeneration(synced.Index, synced.Mutation, synced.Traits) if info.generation == nil then snapshot.incomeKnown = false else snapshot.income += info.generation end elseif objectText ~= "" then info.state = "occupied" info.syncedIndex = objectText info.generation = Farm.getGeneration(objectText, nil, nil) if info.generation == nil then snapshot.incomeKnown = false else snapshot.income += info.generation end end if not slot or info.state == "unresolved" then snapshot.allResolved = false snapshot.incomeKnown = false end snapshot.slots[index] = info end Farm.snapshotCache = snapshot Farm.snapshotCacheDirty = false return snapshot end function Farm.markPromptDirty() Farm.promptCacheDirty = true end function Farm.watchAnimal(animal) if Farm.watchedAnimals[animal] then return end Farm.watchedAnimals[animal] = { animal.DescendantAdded:Connect(Farm.markPromptDirty), animal.DescendantRemoving:Connect(Farm.markPromptDirty), } end function Farm.isPurchasePrompt(prompt) return prompt and prompt.Parent and prompt:IsA("ProximityPrompt") and prompt.ActionText == "Purchase" end function Farm.getPurchasePrompt(animal) local part = animal and animal:FindFirstChild("Part") local attachment = part and part:FindFirstChild("PromptAttachment") local prompt = attachment and attachment:FindFirstChild("ProximityPrompt") return Farm.isPurchasePrompt(prompt) and prompt or nil end function Farm.rebuildPrompts() Farm.promptCache = {} local live = {} for _, animal in ipairs(CollectionService:GetTagged("Animal")) do live[animal] = true Farm.watchAnimal(animal) local prompt = Farm.getPurchasePrompt(animal) if prompt then table.insert(Farm.promptCache, prompt) end end for animal, connections in pairs(Farm.watchedAnimals) do if not live[animal] then for _, connection in ipairs(connections) do connection:Disconnect() end Farm.watchedAnimals[animal] = nil end end for prompt in pairs(Farm.promptRetryAt) do if not Farm.isPurchasePrompt(prompt) then Farm.promptRetryAt[prompt] = nil end end Farm.promptCacheDirty = false end function Farm.getPrompts() if Farm.promptCacheDirty then Farm.rebuildPrompts() end local valid = {} for _, prompt in ipairs(Farm.promptCache) do if Farm.isPurchasePrompt(prompt) then table.insert(valid, prompt) else Farm.promptCacheDirty = true end end Farm.promptCache = valid return valid end function Farm.getPurchasePart(prompt) local part = Farm.isPurchasePrompt(prompt) and prompt.Parent and prompt.Parent.Parent return part and part:IsA("BasePart") and part or nil end function Farm.getCarpets() if Farm.carpetCacheDirty then Farm.carpetCache = {} for _, object in ipairs(Workspace:GetDescendants()) do if object.Name == "Carpet" and object:IsA("BasePart") then table.insert(Farm.carpetCache, object) end end Farm.carpetCacheDirty = false end return Farm.carpetCache end function Farm.findCarpetForPart(part) if not part or not part:IsA("BasePart") or not part:IsDescendantOf(Workspace) then return nil end for _, carpet in ipairs(Farm.getCarpets()) do if carpet.Parent and carpet.Name == "Carpet" and carpet:IsDescendantOf(Workspace) then local offset = carpet.CFrame:PointToObjectSpace(part.Position) if math.abs(offset.X) <= carpet.Size.X * 0.5 + 1.5 and math.abs(offset.Z) <= carpet.Size.Z * 0.5 + 1.5 then return carpet end end end return nil end function Farm.isOnCarpet(part) return Farm.findCarpetForPart(part) ~= nil end function Farm.getCarpetHead(targetPart) local _, _, hrp = Farm.getMovementRefs() if not hrp then return nil end local nearest = Farm.findCarpetForPart(targetPart) local nearestDistance = nearest and (hrp.Position - nearest.Position).Magnitude or math.huge if not nearest then for _, carpet in ipairs(Farm.getCarpets()) do if carpet.Parent and carpet.Name == "Carpet" and carpet:IsDescendantOf(Workspace) then local distance = (hrp.Position - carpet.Position).Magnitude if distance < nearestDistance then nearest, nearestDistance = carpet, distance end end end end if not nearest then return nil end local alongX = nearest.Size.X >= nearest.Size.Z local halfLength = (alongX and nearest.Size.X or nearest.Size.Z) * 0.5 local endOffset = -math.max(0, halfLength - math.min(Farm.CARPET_HEAD_INSET, halfLength * 0.2)) local localPoint = alongX and Vector3.new(endOffset, nearest.Size.Y * 0.5, 0) or Vector3.new(0, nearest.Size.Y * 0.5, endOffset) return nearest.CFrame:PointToWorldSpace(localPoint) end function Farm.promptMatches(prompt, name) if not Farm.isPurchasePrompt(prompt) then return false end local expected = name:lower() return tostring(prompt.ObjectText or ""):lower():find(expected, 1, true) ~= nil or tostring(prompt.Name or ""):lower():find(expected, 1, true) ~= nil end function Farm.getPromptAnimal(prompt) local animal = Farm.getPurchasePart(prompt) while animal and animal ~= Workspace and not CollectionService:HasTag(animal, "Animal") do animal = animal.Parent end return animal ~= Workspace and animal or nil end function Farm.getPromptIncome(prompt, name) if not Farm.promptMatches(prompt, name) then return nil end local animal = Farm.getPromptAnimal(prompt) if not animal then return nil end local mutation = animal:GetAttribute("__mutation") or animal:GetAttribute("Mutation") local traits, seen = {}, {} local function addTrait(traitName) if traitName and traitName ~= "" and not seen[traitName] then seen[traitName] = true table.insert(traits, traitName) end end for _, child in ipairs(animal:GetChildren()) do if not mutation and child.Name:sub(1, 9) == "Mutation." then mutation = child.Name:sub(10) else addTrait(child.Name:match("^_?Trait%.(.+)$")) end end local attribute = animal:GetAttribute("Traits") or animal:GetAttribute("Trait") if type(attribute) == "string" then for traitName in attribute:gmatch("[^,]+") do addTrait((traitName:gsub("^%s*(.-)%s*$", "%1"))) end end return Farm.getGeneration(name, mutation, traits) end function Farm.targetIsCurrent(target) if target == nil then return false end if typeof(target) == "Instance" then return target == Farm.player end if type(target) == "number" then return target == Farm.player.UserId end if type(target) == "string" then return target == Farm.player.Name or tonumber(target) == Farm.player.UserId end return nil end function Farm.targetAllowed(prompt) return Farm.targetIsCurrent(prompt:GetAttribute("TargetPlayer")) == false end function Farm.promptLive(prompt, item) local part = Farm.getPurchasePart(prompt) return Farm.promptMatches(prompt, item.name) and prompt.Enabled == true and Farm.targetAllowed(prompt) and Farm.isOnCarpet(part) and Farm.getCash() >= item.cost end function Farm.getRequiredName(name) for _, item in ipairs(Farm.items) do if item.required and name:lower():find(item.name:lower(), 1, true) then return item.name end end return nil end function Farm.slotIdentity(info) if not info or info.state == "unresolved" then return nil end return info.state .. "\0" .. tostring(info.syncedIndex or info.objectText or "") end function Farm.slotMatchesName(info, name) if not info or info.state ~= "occupied" then return false end local identity = tostring(info.syncedIndex or info.objectText or ""):lower() return identity:find(name:lower(), 1, true) ~= nil end function Farm.reconcileReservations(snapshot) if not next(Farm.reservations) then return end snapshot = snapshot or Farm.snapshot() local confirmed = {} for prompt, reservation in pairs(Farm.reservations) do if Farm.isPurchasePrompt(prompt) and Farm.targetIsCurrent(prompt:GetAttribute("TargetPlayer")) == true then if not reservation.seenInbound then reservation.expiresAt = tick() + Farm.RESERVATION_TIMEOUT end reservation.seenInbound = true reservation.contesting = false end end for index = 1, 10 do local info = snapshot.slots[index] local identity = Farm.slotIdentity(info) if identity and info.state == "occupied" then local oldestPrompt, oldestTime, oldestInbound = nil, math.huge, false for prompt, reservation in pairs(Farm.reservations) do local eligible = not confirmed[prompt] and reservation.baselineSlots[index] ~= identity and Farm.slotMatchesName(info, reservation.name) if eligible and ( (reservation.seenInbound and not oldestInbound) or (reservation.seenInbound == oldestInbound and reservation.createdAt < oldestTime) ) then oldestPrompt, oldestTime, oldestInbound = prompt, reservation.createdAt, reservation.seenInbound end end if oldestPrompt then confirmed[oldestPrompt] = true end end end for prompt in pairs(confirmed) do Farm.reservations[prompt] = nil end local now = tick() local expired = {} for prompt, reservation in pairs(Farm.reservations) do for index = 1, 10 do local identity = Farm.slotIdentity(snapshot.slots[index]) if identity then reservation.baselineSlots[index] = identity end end if not reservation.contesting and now >= reservation.expiresAt then if Farm.rangeResolved(snapshot, 1, 10) then table.insert(expired, prompt) else reservation.expiresAt = now + Farm.TRANSIENT_RETRY end end end for _, prompt in ipairs(expired) do local reservation = Farm.reservations[prompt] if reservation then Farm.reservations[prompt] = nil Farm.promptRetryAt[prompt] = now + Farm.TRANSIENT_RETRY warn("Purchase camp chưa xác nhận; tiếp tục loop.") end end end function Farm.campCounts(snapshot) Farm.reconcileReservations(snapshot) local occupied = 0 for index = 1, 10 do if snapshot.slots[index].state == "occupied" then occupied += 1 end end local pending = {} for prompt, reservation in pairs(Farm.reservations) do if not reservation.contesting or reservation.seenInbound then pending[prompt] = true end end for _, prompt in ipairs(Farm.getPrompts()) do local name = Farm.getRequiredName(tostring(prompt.ObjectText or "")) if name and Farm.targetIsCurrent(prompt:GetAttribute("TargetPlayer")) == true then pending[prompt] = true end end local pendingCount = 0 for _ in pairs(pending) do pendingCount += 1 end return occupied, pendingCount end function Farm.hasLiveContestPrompt(snapshot) local occupied, pending = Farm.campCounts(snapshot) if Farm.stoppedReason or occupied + pending >= Farm.CAMP_SELL_THRESHOLD then return false end for _, item in ipairs(Farm.items) do if item.required and Farm.getCash() >= item.cost then for _, prompt in ipairs(Farm.getPrompts()) do if Farm.promptLive(prompt, item) then return true end end end end return false end function Farm.rangeResolved(snapshot, firstIndex, lastIndex) for index = firstIndex, lastIndex do if snapshot.slots[index].state == "unresolved" then return false end end return true end function Farm.rangeEmpty(snapshot, firstIndex, lastIndex) if not Farm.rangeResolved(snapshot, firstIndex, lastIndex) then return false end for index = firstIndex, lastIndex do if snapshot.slots[index].state ~= "empty" then return false end end return true end function Farm.choosePrompt(skipServed) local best, eligible = nil, false local cash = Farm.getCash() local prompts = Farm.getPrompts() for _, item in ipairs(Farm.items) do if cash >= item.cost then for _, prompt in ipairs(prompts) do local retryAt = Farm.promptRetryAt[prompt] if retryAt and tick() >= retryAt then Farm.promptRetryAt[prompt] = nil retryAt = nil end local reservation = Farm.reservations[prompt] local resumable = reservation and reservation.contesting and reservation.name == item.name if not retryAt and (not reservation or resumable) and Farm.promptLive(prompt, item) then local income = Farm.getPromptIncome(prompt, item.name) if income and income > 0 then eligible = true if (not skipServed or not Farm.contestServed[prompt]) and ( not best or income > best.income or (income == best.income and item.cost < best.item.cost) ) then best = { item = item, prompt = prompt, income = income } end end end end end end return best, eligible end function Farm.selectPrompt() Farm.reconcileReservations() for prompt in pairs(Farm.contestServed) do if not Farm.isPurchasePrompt(prompt) then Farm.contestServed[prompt] = nil end end local best, eligible = Farm.choosePrompt(true) if not best and eligible and next(Farm.contestServed) then Farm.contestServed = {} best = Farm.choosePrompt(true) end return best end function Action.closeCoinsShop(close) if not close then close = select(6, Farm.getSpeedCoilUi()) end if type(firesignal) == "function" and close and close.Parent and close:IsA("GuiButton") then pcall(firesignal, close.Activated) end end function Action.waitSpeedCoilStep(duration, deadline, plot, jobId, character) local stopAt = tick() + duration if stopAt > deadline then return false end while tick() < stopAt do local liveCharacter = Farm.getMovementRefs() if game.JobId ~= jobId or liveCharacter ~= character or not Farm.farmReady(plot) then return false end task.wait(0.1) end local liveCharacter = Farm.getMovementRefs() return game.JobId == jobId and liveCharacter == character and Farm.farmReady(plot) end function Action.equipSpeedCoilIfOwned() local tool = Farm.getTool(Farm.SPEED_COIL_NAME) if not tool then return false end Farm.speedCoilDone = true Farm.speedCoilUnknown = false local character, humanoid = Farm.getMovementRefs() if not character or not humanoid then return false end if tool.Parent == character then return true end if tick() < Farm.speedCoilEquipRetryAt then return false end local backpack = Farm.player and Farm.player:FindFirstChildOfClass("Backpack") if tool.Parent ~= backpack then Farm.speedCoilEquipRetryAt = tick() + Farm.TRANSIENT_RETRY return false end local equipped = pcall(humanoid.EquipTool, humanoid, tool) if not equipped then Farm.speedCoilEquipRetryAt = tick() + Farm.TRANSIENT_RETRY return false end local deadline = tick() + 2 while tick() < deadline do local liveCharacter = Farm.player and Farm.player.Character if liveCharacter ~= character then break end if tool.Parent == character then return true end task.wait(0.1) end Farm.speedCoilEquipRetryAt = tick() + Farm.TRANSIENT_RETRY return false end function Action.buySpeedCoilIfReady(plot) local ownedTool = Farm.getTool(Farm.SPEED_COIL_NAME) if ownedTool then local character = Farm.getMovementRefs() if ownedTool.Parent == character then Action.equipSpeedCoilIfOwned() elseif character and tick() >= Farm.speedCoilEquipRetryAt then local deadline = tick() + Farm.SHOP_EQUIP_DELAY + 1 if Action.waitSpeedCoilStep(Farm.SHOP_EQUIP_DELAY, deadline, plot, game.JobId, character) then Action.equipSpeedCoilIfOwned() end end return false end local ready, expectedPrice = Farm.speedCoilReady() if not ready then return false end if type(firesignal) ~= "function" then Farm.speedCoilDisabled = true warn("Speed Coil tự mua bị tắt: firesignal không khả dụng.") return false end Farm.setStatus("Đang mua Speed Coil") local deadline = tick() + Farm.SHOP_TIMEOUT local startJobId = game.JobId local closeButton local function retry() Action.closeCoinsShop(closeButton) Farm.speedCoilRetryAt = tick() + Farm.TRANSIENT_RETRY return true end local npc, prompt, target = Farm.getCoinsShopRefs() local screen, panel, item, buy, priceLabel screen, panel, item, buy, priceLabel, closeButton = Farm.getSpeedCoilUi() local startCharacter, _, hrp = Farm.getMovementRefs() if not npc or not prompt or not target or not screen or not panel or not item or not buy or not priceLabel or not closeButton or not startCharacter or not hrp or not Farm.farmReady(plot) or game.JobId ~= startJobId then return retry() end local maxDistance = tonumber(prompt.MaxActivationDistance) or 15 local flat = Vector3.new(hrp.Position.X - target.Position.X, 0, hrp.Position.Z - target.Position.Z) local direction = flat.Magnitude > 0.1 and flat.Unit or Vector3.new(0, 0, 1) local approach = target.Position + direction * math.max(3, maxDistance * 0.6) approach = Vector3.new(approach.X, hrp.Position.Y, approach.Z) if not Action.pathfindTo(approach, deadline, plot, startCharacter) or not Action.moveTo(approach, deadline, plot, 3, startCharacter) then return retry() end if not Action.waitSpeedCoilStep(Farm.SHOP_PROMPT_DELAY, deadline, plot, startJobId, startCharacter) then return retry() end local currentCharacter, _, currentHrp = Farm.getMovementRefs() if game.JobId ~= startJobId or currentCharacter ~= startCharacter or not currentHrp or not Farm.farmReady(plot) or prompt.Parent ~= npc or prompt.Enabled ~= true or prompt.ActionText ~= "Coins Shop" or (currentHrp.Position - target.Position).Magnitude > maxDistance then return retry() end if not (screen.Enabled and panel.Visible) then local opened = pcall(function() fireproximityprompt(prompt) end) if not opened then return retry() end end local openDeadline = math.min(deadline, tick() + 5) while tick() < openDeadline do local liveScreen, livePanel, liveItem, liveBuy, livePrice, liveClose = Farm.getSpeedCoilUi() if liveClose then closeButton = liveClose end if liveScreen and liveScreen.Enabled and livePanel and livePanel.Visible then screen, panel, item, buy, priceLabel = liveScreen, livePanel, liveItem, liveBuy, livePrice break end if not Farm.farmReady(plot) or game.JobId ~= startJobId then return retry() end task.wait(0.1) end if not Action.waitSpeedCoilStep(Farm.SHOP_BUY_DELAY, deadline, plot, startJobId, startCharacter) then return retry() end local liveNpc, livePrompt, liveTarget = Farm.getCoinsShopRefs() local liveScreen, livePanel, liveItem, liveBuy, livePrice, liveClose = Farm.getSpeedCoilUi() local liveCharacter, _, liveHrp = Farm.getMovementRefs() local playerData = Farm.player and Farm.getSyncedTable(Farm.player) local coins = playerData and tonumber(playerData.Coins) local displayedPrice = livePrice and Farm.parseCash(tostring(livePrice.Text or ""):gsub("%$", "")) or 0 if liveClose then closeButton = liveClose end if game.JobId ~= startJobId or liveCharacter ~= startCharacter or not liveHrp or not Farm.farmReady(plot) or liveNpc ~= npc or livePrompt ~= prompt or liveTarget ~= target or (liveHrp.Position - liveTarget.Position).Magnitude > maxDistance or not liveScreen or not liveScreen.Enabled or not livePanel or not livePanel.Visible or liveItem ~= item or not liveItem.Visible or liveBuy ~= buy or not liveBuy.Visible or not liveBuy.Active or not liveBuy.Interactable or livePrice ~= priceLabel or liveBuy.ImageColor3 ~= Color3.fromRGB(255, 255, 255) or displayedPrice ~= expectedPrice or not coins or coins < expectedPrice or Farm.hasTool(Farm.SPEED_COIL_NAME) then if Farm.hasTool(Farm.SPEED_COIL_NAME) then Farm.speedCoilDone = true end return retry() end Farm.speedCoilUnknown = true local clicked = pcall(firesignal, buy.Activated) if not clicked then Action.closeCoinsShop(closeButton) Farm.setStatus("Kết quả mua Speed Coil chưa rõ") warn("Speed Coil signal lỗi sau khi bắt đầu; không tự thử lại.") return true end local confirmDeadline = math.min(deadline, tick() + 5) while tick() < confirmDeadline do if Farm.hasTool(Farm.SPEED_COIL_NAME) then Farm.speedCoilDone = true Farm.speedCoilUnknown = false Action.closeCoinsShop(closeButton) local paced = Action.waitSpeedCoilStep(Farm.SHOP_EQUIP_DELAY, deadline, plot, startJobId, startCharacter) local equipped = paced and Action.equipSpeedCoilIfOwned() or false Farm.setStatus(equipped and "Đã mua và equip Speed Coil" or "Đã mua Speed Coil; đang chờ equip") return true end task.wait(0.1) end Action.closeCoinsShop(closeButton) Farm.setStatus("Kết quả mua Speed Coil chưa rõ") warn("Speed Coil đã được bấm mua nhưng chưa xác nhận; không tự thử lại.") return true end function Action.getPromptPart(prompt) return prompt and prompt:FindFirstAncestorWhichIsA("BasePart") or nil end function Action.validSellPrompt(prompt, info) return prompt and prompt.Parent and prompt:IsA("ProximityPrompt") and prompt.Enabled == true and prompt:IsDescendantOf(info.slot) and tostring(prompt.ActionText or ""):sub(1, 4) == "Sell" end function Action.findSellPrompt(info) for _, object in ipairs(info.slot:GetDescendants()) do if Action.validSellPrompt(object, info) then return object end end return nil end function Action.sameIdentity(expected, current) return current and current.state == "occupied" and current.slot == expected.slot and expected.syncedIndex ~= nil and current.syncedIndex == expected.syncedIndex and expected.objectText ~= "" and current.objectText == expected.objectText end function Action.sell(expected, preFireCheck) if expected.state ~= "occupied" or not expected.slot or expected.objectText == "" or not expected.syncedIndex then return false end local plot = Farm.findMyPlot() local prompt = Action.findSellPrompt(expected) local spawn = expected.slot:FindFirstChild("Base") spawn = spawn and spawn:FindFirstChild("Spawn") if not prompt or not spawn or not spawn:IsA("BasePart") then return false end local deadline = tick() + Farm.ACTION_TIMEOUT if not Farm.farmReady(plot) or not Action.pathfindTo(spawn.Position, deadline, plot) then return false end if not Farm.farmReady(plot) or not Action.moveTo(spawn.Position, deadline, plot) then return false end local live = Farm.snapshot() local liveInfo = live.slots[expected.index] local promptPart = Action.getPromptPart(prompt) local _, _, hrp = Farm.getMovementRefs() local maxDistance = tonumber(prompt.MaxActivationDistance) or 10 if not Farm.farmReady(plot) or live.plot ~= plot or not Action.sameIdentity(expected, liveInfo) or not Action.validSellPrompt(prompt, liveInfo) or not promptPart or not hrp or (hrp.Position - promptPart.Position).Magnitude > maxDistance then return false end if preFireCheck then local ok, allowed = pcall(preFireCheck) if not ok or not allowed then return false end end local finalInfo = Farm.snapshot().slots[expected.index] local finalPart = Action.getPromptPart(prompt) local _, _, finalHrp = Farm.getMovementRefs() if not Farm.farmReady(plot) or not Action.sameIdentity(expected, finalInfo) or not Action.validSellPrompt(prompt, finalInfo) or not finalPart or not finalHrp or (finalHrp.Position - finalPart.Position).Magnitude > (tonumber(prompt.MaxActivationDistance) or 10) then return false end local fired = pcall(function() fireproximityprompt(prompt) end) if not fired then return false end Farm.markSnapshotDirty() local confirmDeadline = tick() + 4 while tick() < confirmDeadline do if not Farm.farmReady(plot) then return false end local current = Farm.snapshot().slots[expected.index] if current.state == "empty" or (current.state == "occupied" and not Action.sameIdentity(expected, current)) then return true end task.wait(Farm.SNAPSHOT_POLL) end Farm.actionRetryAt = tick() + Farm.TRANSIENT_RETRY Farm.setStatus("Sell chưa xác nhận, đang đồng bộ") warn("Sell chưa xác nhận ở slot " .. expected.index .. "; tiếp tục loop.") return false end function Action.purchaseStillAllowed(item, prompt) local plot = Farm.findMyPlot() if Farm.stoppedReason or not Farm.farmReady(plot) or not Farm.promptLive(prompt, item) then return false end local snapshot = Farm.snapshot() if snapshot.plot ~= plot or not snapshot.incomeKnown or not Farm.rangeResolved(snapshot, 1, 10) then return false end local occupied, pending = Farm.campCounts(snapshot) return occupied + pending < Farm.CAMP_SELL_THRESHOLD end function Action.followPurchaseTarget(item, prompt, deadline, plot, startCharacter) if not startCharacter then return false end local lastGoal = nil local lastMoveAt = 0 local nextValidation = 0 while tick() < deadline do local now = tick() if Farm.stoppedReason or not Farm.farmReady(plot) or not Farm.promptLive(prompt, item) or (now >= nextValidation and not Action.purchaseStillAllowed(item, prompt)) then Farm.stopMovement() return false end if now >= nextValidation then nextValidation = now + Farm.ACTION_VALIDATE_INTERVAL end local part = Farm.getPurchasePart(prompt) local character, humanoid, hrp = Farm.getMovementRefs() if character ~= startCharacter or not humanoid or not hrp or not part then Farm.stopMovement() return false end local maxDistance = tonumber(prompt.MaxActivationDistance) or 10 if (hrp.Position - part.Position).Magnitude <= maxDistance then Farm.stopMovement() return true end local goal = part.Position if not lastGoal or (goal - lastGoal).Magnitude >= 0.5 or now - lastMoveAt >= 0.5 then Farm.setFps(Farm.MOVE_FPS) humanoid:MoveTo(goal) lastGoal = goal lastMoveAt = now end task.wait(Farm.ACTION_POLL) end Farm.stopMovement() return false end function Action.buy(item, prompt) if not Action.purchaseStillAllowed(item, prompt) then return false end Farm.contestServed[prompt] = true Farm.setFps(Farm.MOVE_FPS) local plot = Farm.findMyPlot() local part = Farm.getPurchasePart(prompt) local deadline = tick() + Farm.ACTION_TIMEOUT local startCharacter, _, hrp = Farm.getMovementRefs() if not part or not hrp then return false end local maxDistance = tonumber(prompt.MaxActivationDistance) or 10 local carpet = Farm.findCarpetForPart(part) if carpet and (hrp.Position - part.Position).Magnitude > maxDistance + 8 then Action.pathfindTo(carpet.Position, math.min(deadline, tick() + 8), plot, startCharacter) end local currentCharacter = Farm.getMovementRefs() if currentCharacter ~= startCharacter or not Action.followPurchaseTarget(item, prompt, deadline, plot, startCharacter) then return false end part = Farm.getPurchasePart(prompt) local _, _, latestHrp = Farm.getMovementRefs() maxDistance = tonumber(prompt.MaxActivationDistance) or 10 if not Action.purchaseStillAllowed(item, prompt) or not part or not latestHrp or (latestHrp.Position - part.Position).Magnitude > maxDistance then return false end local reservation = Farm.reservations[prompt] if reservation and not (reservation.contesting and reservation.name == item.name) then return false end if not reservation then local baseline = Farm.snapshot() if baseline.plot ~= plot then return false end local baselineSlots = {} for index = 1, 10 do local identity = Farm.slotIdentity(baseline.slots[index]) if not identity then return false end baselineSlots[index] = identity end local now = tick() reservation = { name = item.name, createdAt = now, expiresAt = now + Farm.RESERVATION_TIMEOUT, seenInbound = false, contesting = true, attempted = false, baselineSlots = baselineSlots, } Farm.reservations[prompt] = reservation end do local contestDeadline = math.min(deadline, tick() + Farm.CONTEST_SLICE) local contestOk, claimed = pcall(function() local nextValidation = 0 while tick() < contestDeadline do if Farm.targetIsCurrent(prompt:GetAttribute("TargetPlayer")) == true then return true end local now = tick() part = Farm.getPurchasePart(prompt) local _, _, currentHrp = Farm.getMovementRefs() if not part or not currentHrp or (currentHrp.Position - part.Position).Magnitude > (tonumber(prompt.MaxActivationDistance) or 10) then return false end if now >= nextValidation then if not Action.purchaseStillAllowed(item, prompt) then return false end nextValidation = now + Farm.ACTION_VALIDATE_INTERVAL end reservation.attempted = true local fired = pcall(function() fireproximityprompt(prompt) end) if not fired then return false end task.wait(Farm.ACTION_POLL) end return false end) if not contestOk then Farm.actionRetryAt = tick() + Farm.TRANSIENT_RETRY warn("Contest purchase lỗi tạm thời; tiếp tục loop.") end if claimed ~= true and Farm.targetIsCurrent(prompt:GetAttribute("TargetPlayer")) == true then claimed = true end if claimed == true then reservation.seenInbound = true reservation.contesting = false reservation.expiresAt = tick() + Farm.RESERVATION_TIMEOUT Farm.markSnapshotDirty() return true end if Farm.reservations[prompt] == reservation then local livePart = Farm.getPurchasePart(prompt) local stillOffered = contestOk and reservation.attempted and Farm.promptMatches(prompt, item.name) and prompt.Enabled == true and Farm.targetIsCurrent(prompt:GetAttribute("TargetPlayer")) == false and Farm.isOnCarpet(livePart) if stillOffered then Farm.reservations[prompt] = nil Farm.promptRetryAt[prompt] = tick() + Farm.CONTEST_RETRY elseif reservation.attempted then reservation.contesting = false reservation.expiresAt = tick() + Farm.RESERVATION_TIMEOUT else Farm.reservations[prompt] = nil end end return false end end function Action.cleanup(snapshot, firstIndex, lastIndex) local plot = snapshot.plot for index = firstIndex or 6, lastIndex or 10 do local info = snapshot.slots[index] if info.state == "occupied" then if snapshot.plot ~= plot or not Farm.farmReady(plot) or not Action.sell(info) then return false end snapshot = Farm.snapshot() end end return true end function Action.returnToCarpetHead(plot, interruptForContest) local position = Farm.getCarpetHead() if not position then return false end local candidate, nextCheck = nil, 0 local function contestReady() local now = tick() if now < nextCheck then return false end nextCheck = now + Farm.CONTEST_SCAN_INTERVAL candidate = Farm.selectPrompt() if candidate then return true end local snapshot = Farm.snapshot() return Farm.hasLiveContestPrompt(snapshot) end local interruptCheck = interruptForContest and contestReady or nil local reached = Action.pathfindTo(position, tick() + Farm.RETURN_TIMEOUT, plot, nil, interruptCheck) return reached, candidate end function Fleet.resolveRequest() if type(request) == "function" then return request end if type(http_request) == "function" then return http_request end if type(http) == "table" and type(http.request) == "function" then return http.request end if type(syn) == "table" and type(syn.request) == "function" then return syn.request end local env = type(getgenv) == "function" and getgenv() or nil return env and type(env.request) == "function" and env.request or nil end function Fleet.http(url, method, payload, timeout) if not Fleet.requestFn then return nil, "request API unavailable" end local requestTimeout = timeout or Fleet.REQUEST_TIMEOUT local body if payload ~= nil then local encoded, result = pcall(Fleet.HttpService.JSONEncode, Fleet.HttpService, payload) if not encoded then return nil, "JSON encode failed" end body = result end local options = { Url = url, Method = method, Headers = { ["Content-Type"] = "application/json" }, Body = body, Timeout = requestTimeout, } local ok, response = pcall(Fleet.requestFn, options) if not ok then return nil, tostring(response) end if type(response) ~= "table" then return nil, "invalid response" end local status = tonumber(response.StatusCode or response.Status) if status and (status < 200 or status >= 300) then return nil, "HTTP " .. status end if not status and response.Success ~= true then return nil, "response missing success status" end return tostring(response.Body or response.body or "") end function Fleet.getJson(path, tries, retryGap, timeout) tries = tries or Fleet.REQUEST_TRIES retryGap = retryGap or Fleet.REQUEST_RETRY_GAP for attempt = 1, tries do local raw, problem = Fleet.http(Fleet.API_BASE .. path, "GET", nil, timeout) if raw then local ok, data = pcall(Fleet.HttpService.JSONDecode, Fleet.HttpService, raw) if ok and type(data) == "table" then return data end problem = "invalid JSON" end warn(("Fleet GET %s lỗi %d/%d: %s"):format(path, attempt, tries, tostring(problem))) if attempt < tries then task.wait(retryGap * attempt + Fleet.rng:NextNumber(0, 1)) end end return nil end function Fleet.postPresence() local player = Farm.player if not player then return false end local payload = { account = player.Name, placeId = Fleet.PLACE_ID, jobId = tostring(game.JobId), players = math.min(200, #Players:GetPlayers()), } local raw, problem = Fleet.http(Fleet.API_BASE .. "/presence", "POST", payload) if raw ~= nil then return true end warn("Fleet POST /presence lỗi; không retry: " .. tostring(problem)) return false end function Fleet.usernamesToSet(list) local usernames, count = {}, 0 for _, entry in ipairs(list) do local name = type(entry) == "table" and entry.username or entry if type(name) == "string" and #name >= 1 and #name <= 20 and name:match("^[A-Za-z0-9_]+$") and not usernames[name:lower()] then usernames[name:lower()] = true count += 1 end end return usernames, count end function Fleet.registryCachePath() return ("WN_registry_%d.json"):format(Fleet.PLACE_ID) end function Fleet.readRegistryCache() if not Fleet.registryCacheAvailable then return nil end local ok, raw = pcall(function() local path = Fleet.registryCachePath() return isfile(path) and readfile(path) or nil end) if not ok or type(raw) ~= "string" or raw == "" then return nil end local decoded, data = pcall(Fleet.HttpService.JSONDecode, Fleet.HttpService, raw) if not decoded or type(data) ~= "table" or type(data.usernames) ~= "table" or type(data.exp) ~= "number" then return nil end local usernames, count = Fleet.usernamesToSet(data.usernames) return usernames, data.exp, count end function Fleet.writeRegistryCache(usernames, expiresAt) if not Fleet.registryCacheAvailable or type(usernames) ~= "table" then return end pcall(function() writefile( Fleet.registryCachePath(), Fleet.HttpService:JSONEncode({ exp = expiresAt, usernames = usernames }) ) end) end function Fleet.fetchUsernames() local path = ("/usernames/%d"):format(Fleet.PLACE_ID) for attempt = 1, Fleet.USERNAME_TRIES do local raw, problem = Fleet.http(Fleet.API_BASE .. path, "GET", nil, Fleet.USERNAME_REQUEST_TIMEOUT) if raw then local decoded, data = pcall(Fleet.HttpService.JSONDecode, Fleet.HttpService, raw) if decoded and type(data) == "table" and type(data.usernames) == "table" then local usernames, count = Fleet.usernamesToSet(data.usernames) return usernames, count, data.usernames end problem = "invalid usernames JSON" end warn(("Fleet GET %s lỗi %d/%d: %s"):format(path, attempt, Fleet.USERNAME_TRIES, tostring(problem))) if attempt < Fleet.USERNAME_TRIES then task.wait(Fleet.USERNAME_RETRY_GAP + Fleet.rng:NextNumber(0, 1)) end end return nil end function Fleet.refreshRegistry() local now = os.time() local ttl = Fleet.USERNAME_CACHE_TTL local jitterSpan = math.max(1, math.min(math.floor(ttl / 2), 2 * Fleet.USERNAME_REFRESH_INTERVAL)) local jitter = (Farm.player and Farm.player.UserId or 0) % jitterSpan if Fleet.registryReady and Fleet.registryCount > 0 and now < Fleet.registryExpiresAt - jitter then return true, Fleet.registryCount end local cached, cachedExpiresAt, cachedCount = Fleet.readRegistryCache() if cached and cachedCount > 0 and cachedExpiresAt and now < cachedExpiresAt - jitter then Fleet.registry = cached Fleet.registryReady = true Fleet.registryCount = cachedCount Fleet.registryExpiresAt = cachedExpiresAt return true, cachedCount end local usernames, count, rawUsernames = Fleet.fetchUsernames() if usernames then if count > 0 then local cacheNow = os.time() -- ponytail: file API không có create-exclusive; recheck giảm ghi trùng, dùng atomic lock nếu executor hỗ trợ. local shared, sharedExpiresAt, sharedCount = Fleet.readRegistryCache() if shared and sharedCount > 0 and sharedExpiresAt and cacheNow < sharedExpiresAt - jitter then Fleet.registry = shared Fleet.registryReady = true Fleet.registryCount = sharedCount Fleet.registryExpiresAt = sharedExpiresAt return true, sharedCount end local expiresAt = cacheNow + ttl Fleet.registry = usernames Fleet.registryReady = true Fleet.registryCount = count Fleet.registryExpiresAt = expiresAt Fleet.writeRegistryCache(rawUsernames, expiresAt) elseif not Fleet.registryReady then Fleet.registry = {} Fleet.registryReady = true Fleet.registryCount = 0 Fleet.registryExpiresAt = 0 end return true, count end if Fleet.registryReady then return true, Fleet.registryCount end if cached and cachedCount > 0 then Fleet.registry = cached Fleet.registryReady = true Fleet.registryCount = cachedCount Fleet.registryExpiresAt = cachedExpiresAt or now return true, cachedCount end return false, 0 end function Fleet.playerInRegistry(player, usernames) if not player or not Farm.player or player.UserId == Farm.player.UserId then return false end return usernames[player.Name:lower()] == true end function Fleet.friendStandoff(usernames) for _, other in ipairs(Players:GetPlayers()) do if Fleet.playerInRegistry(other, usernames) then return other end end return nil end function Fleet.findRichPlayer() local config = getgenv().CarpetFarmConfig or {} local threshold = math.max(0, tonumber(config.RichPlayerCash) or 20000000) for _, other in ipairs(Players:GetPlayers()) do if other ~= Farm.player then local leaderstats = other:FindFirstChild("leaderstats") local cashValue = leaderstats and leaderstats:FindFirstChild("Cash") local cash = cashValue and Farm.parseCash(cashValue.Value) or 0 if cash > threshold then return other, cash end end end return nil, 0 end function Fleet.fetchJobs() local data = Fleet.getJson(("/jobs/%d"):format(Fleet.PLACE_ID), 1) if not data or type(data.servers) ~= "table" then return nil end local jobs, seen = {}, {} for index = 1, math.min(#data.servers, Fleet.MAX_ITEMS) do local server = data.servers[index] local jobId = type(server) == "table" and server.job_id or nil local playing = type(server) == "table" and tonumber(server.playing) or nil local maxPlayers = type(server) == "table" and tonumber(server.max_players) or nil if type(jobId) == "string" and #jobId >= 1 and #jobId <= 64 and jobId:match("^[A-Za-z0-9%-]+$") and (not playing or not maxPlayers or maxPlayers <= 0 or playing < maxPlayers) and jobId ~= game.JobId and not Fleet.tried[jobId] and not seen[jobId] then seen[jobId] = true jobs[#jobs + 1] = jobId end end return jobs end function Fleet.shuffle(list) for index = #list, 2, -1 do local other = Fleet.rng:NextInteger(1, index) list[index], list[other] = list[other], list[index] end end function Fleet.postJob(kind, jobId) return Fleet.http(("%s/jobs/%d/%s"):format(Fleet.API_BASE, Fleet.PLACE_ID, kind), "POST", { job_id = jobId }) ~= nil end function Fleet.queueJobReport(kind, jobId) Fleet.jobReportQueue[#Fleet.jobReportQueue + 1] = { kind = kind, jobId = jobId } if Fleet.jobReportActive then return end Fleet.jobReportActive = true task.spawn(function() while Farm.isCurrentRun() and Fleet.jobReportHead <= #Fleet.jobReportQueue do local report = Fleet.jobReportQueue[Fleet.jobReportHead] Fleet.jobReportHead += 1 if not Fleet.postJob(report.kind, report.jobId) then warn("Fleet không post " .. report.kind .. " cho job: " .. tostring(report.jobId)) end task.wait() end Fleet.jobReportQueue = {} Fleet.jobReportHead = 1 Fleet.jobReportActive = false end) end function Fleet.reportDeadJob(jobId) Fleet.queueJobReport("dead", jobId) end function Fleet.clearTeleportError() pcall(Fleet.GuiService.ClearError, Fleet.GuiService) end function Fleet.bindTeleportSignals() for _, connection in ipairs(Fleet.teleportConnections) do connection:Disconnect() end Fleet.teleportConnections = { Fleet.TeleportService.TeleportInitFailed:Connect(function(player, result) if Fleet.currentJobId and player and Farm.player and player.UserId == Farm.player.UserId then Fleet.teleportFailed = true Fleet.teleportResult = result Fleet.clearTeleportError() warn("Fleet teleport fail " .. tostring(result) .. "; thử JobId kế tiếp.") end end), Farm.player.OnTeleport:Connect(function(state) if Fleet.currentJobId and (state == Enum.TeleportState.Started or state == Enum.TeleportState.InProgress) then Fleet.teleportStarted = true end end), } end function Fleet.attemptJob(jobId) if not Farm.isCurrentRun() or Farm.stoppedReason or not jobId or jobId == game.JobId or Fleet.tried[jobId] then return false end local sourcePlaceId, sourceJobId = game.PlaceId, game.JobId Fleet.tried[jobId] = true Fleet.currentJobId = jobId Fleet.teleportFailed = false Fleet.teleportResult = nil Fleet.teleportStarted = false Fleet.clearTeleportError() Fleet.queueJobReport("blacklist", jobId) local called, callProblem = pcall(Fleet.TeleportService.TeleportToPlaceInstance, Fleet.TeleportService, Fleet.PLACE_ID, jobId, Farm.player) if not called then Fleet.currentJobId = nil warn("Fleet teleport call lỗi; không report dead: " .. tostring(callProblem):sub(1, 300)) return false, "failed" end local deadline = tick() + Fleet.TELEPORT_WAIT while tick() < deadline and not Fleet.teleportFailed and not Fleet.teleportStarted do task.wait(0.1) end if Fleet.teleportStarted then deadline = tick() + Fleet.TELEPORT_CONFIRM while tick() < deadline and not Fleet.teleportFailed and game.PlaceId == sourcePlaceId and game.JobId == sourceJobId do task.wait(0.1) end if game.PlaceId ~= sourcePlaceId or game.JobId ~= sourceJobId then Fleet.currentJobId = nil return true end if not Fleet.teleportFailed then Fleet.currentJobId = nil Fleet.unknownTeleport = true warn("Fleet teleport đã bắt đầu nhưng chưa xác nhận; không teleport hoặc shutdown thêm.") return false, "unknown" end end if Fleet.teleportFailed and Fleet.teleportResult == Enum.TeleportResult.GameEnded then Fleet.reportDeadJob(jobId) end Fleet.currentJobId = nil if not Fleet.teleportFailed then warn("Fleet teleport timeout; không report dead và tiếp tục thử server khác.") end return false, "failed" end function Fleet.fetchRobloxJobs() local jobs, seen, cursor = {}, {}, nil for _ = 1, Fleet.ROBLOX_PAGES do local url = ("https://games.roblox.com/v1/games/%d/servers/Public?limit=100&excludeFullGames=true"):format( Fleet.PLACE_ID ) .. (cursor and ("&cursor=" .. Fleet.HttpService:UrlEncode(cursor)) or "") local raw = Fleet.http(url, "GET") if not raw then break end local ok, data = pcall(Fleet.HttpService.JSONDecode, Fleet.HttpService, raw) if not ok or type(data) ~= "table" or type(data.data) ~= "table" then break end for index = 1, math.min(#data.data, Fleet.MAX_ITEMS) do local server = data.data[index] local jobId = type(server) == "table" and server.id or nil if type(jobId) == "string" and #jobId >= 1 and #jobId <= 64 and jobId:match("^[A-Za-z0-9%-]+$") and jobId ~= game.JobId and not Fleet.tried[jobId] and not seen[jobId] then seen[jobId] = true jobs[#jobs + 1] = jobId end end cursor = type(data.nextPageCursor) == "string" and data.nextPageCursor or nil if not cursor or cursor == "" then break end end return jobs end function Fleet.tryHobeoHop(reason) local jobs = Fleet.fetchJobs() if not jobs or #jobs == 0 then return false end Fleet.shuffle(jobs) local tries = math.min(#jobs, Fleet.HOBEO_HOP_ATTEMPTS) for attempt = 1, tries do if not Farm.isCurrentRun() then return false end local status = reason == "population" and "Đang hop server ít người" or reason == "rich" and "Đang hop tránh player nhiều tiền" or "Đang hop tránh friend clone" Farm.setStatus(("%s qua Hobeo %d/%d"):format(status, attempt, tries)) local joined, outcome = Fleet.attemptJob(jobs[attempt]) if joined then return true end if outcome == "unknown" then return false, outcome end if attempt < tries then task.wait(Fleet.HOBEO_RETRY_GAP) end end return false end function Fleet.tryRobloxFallback() if not Farm.isCurrentRun() or Farm.stoppedReason then return false end Farm.setStatus("Hobeo hết server; đang dùng Roblox hop") local jobs = Fleet.fetchRobloxJobs() Fleet.shuffle(jobs) if not jobs[1] then return false, "empty" end return Fleet.attemptJob(jobs[1]) end function Fleet.shutdownAfterRobloxFailure(problem) Fleet.currentJobId = nil Fleet.hopping = false Farm.stoppedReason = "Roblox hop thất bại; account có thể bị captcha" Farm.stopMovement() Farm.setStatus("Roblox hop thất bại; đang shutdown") warn("Roblox hop không thành công; account có thể bị captcha: " .. tostring(problem):sub(1, 300)) local ok, shutdownProblem = pcall(game.Shutdown, game) if not ok then warn("game:Shutdown() thất bại: " .. tostring(shutdownProblem):sub(1, 300)) end return false, "shutdown" end function Fleet.hop(reason) if Fleet.unknownTeleport then return false, "unknown" end if Fleet.hopping then return false, "busy" end Fleet.hopping = true Fleet.checking = false Farm.stopMovement() local hobeoOk, hobeoJoined, hobeoOutcome = pcall(Fleet.tryHobeoHop, reason) if hobeoOk and hobeoJoined then Fleet.currentJobId = nil Fleet.hopping = false return true end if Fleet.unknownTeleport or hobeoOutcome == "unknown" then Fleet.currentJobId = nil Fleet.hopping = false Farm.stoppedReason = "Teleport đang có kết quả chưa rõ; đã chặn gọi lặp" return false, "unknown" end if not Farm.isCurrentRun() or Farm.stoppedReason then Fleet.currentJobId = nil Fleet.hopping = false return false, "stopped" end if not hobeoOk then warn("Hobeo hop lỗi; chuyển sang Roblox hop: " .. tostring(hobeoJoined):sub(1, 300)) end local robloxOk, robloxJoined, robloxOutcome = pcall(Fleet.tryRobloxFallback) if robloxOk and robloxJoined then Fleet.currentJobId = nil Fleet.hopping = false return true end if Fleet.unknownTeleport or robloxOutcome == "unknown" then Fleet.currentJobId = nil Fleet.hopping = false Farm.stoppedReason = "Roblox teleport đang có kết quả chưa rõ; không shutdown" return false, "unknown" end return Fleet.shutdownAfterRobloxFailure(robloxOk and robloxOutcome or robloxJoined) end function Fleet.tickPresence() local now = tick() if Fleet.presencePostActive or now < Fleet.nextPresenceAt then return end Fleet.presencePostActive = true task.spawn(function() local ok, problem = pcall(Fleet.postPresence) Fleet.nextPresenceAt = tick() + Fleet.PRESENCE_HEARTBEAT Fleet.presencePostActive = false if not ok then warn("Fleet heartbeat lỗi: " .. tostring(problem):sub(1, 300)) end end) end function Fleet.startFriendCheck() if Fleet.friendCheckStarted then return end Fleet.friendCheckStarted = true task.spawn(function() local userIdStagger = (Farm.player.UserId % 17) / 16 * Fleet.USERNAME_STAGGER task.wait(Fleet.USERNAME_SETTLE + userIdStagger + Fleet.rng:NextNumber(0, 0.5)) local nextRefreshAt = 0 while Farm.isCurrentRun() and not Farm.stoppedReason do local ok, problem = pcall(function() if tick() >= nextRefreshAt then local refreshed, usernameCount = Fleet.refreshRegistry() nextRefreshAt = tick() + (refreshed and Fleet.USERNAME_REFRESH_INTERVAL or Fleet.PRESENCE_HEARTBEAT) if not refreshed then warn("Fleet usernames API lỗi; sẽ thử lại sau cooldown.") else warn(("Fleet registry: %d usernames; shared-cache=%s"):format( usernameCount, tostring(Fleet.registryCacheAvailable) )) end end local localConflict = Fleet.friendStandoff(Fleet.registry) if localConflict and localConflict.Parent == Players then Fleet.hop("friend") end end) if not ok then warn("Fleet friend scan lỗi; sẽ thử lại: " .. tostring(problem):sub(1, 300)) end if Farm.isCurrentRun() and not Farm.stoppedReason then task.wait(Fleet.FRIEND_CHECK_INTERVAL) end end end) end function Fleet.start() if Fleet.started then return end if game.PlaceId ~= Fleet.PLACE_ID then Farm.stoppedReason = "Script chỉ hỗ trợ PlaceId " .. Fleet.PLACE_ID return end Fleet.started = true Fleet.requestFn = Fleet.resolveRequest() if not Fleet.requestFn or not Farm.player then warn("Fleet workflow không khả dụng; tiếp tục farm.") return end Fleet.checking = false Fleet.bindTeleportSignals() local presenceStagger = (Farm.player.UserId % 17) / 16 * Fleet.PRESENCE_STAGGER Fleet.nextPresenceAt = tick() + presenceStagger + Fleet.rng:NextNumber(0, 0.5) Fleet.startFriendCheck() end function Action.changeToFolderIfLowCash() local cash = Farm.getCash() if not Farm.cashValue or not Farm.cashValue.Parent or cash >= Farm.LOW_CASH_FOLDER_THRESHOLD then return false end if Farm.changingFolder or Farm.changedFolder or Farm.changeFolderTimedOut then return true end local config = getgenv().RebirthConfig or {} if config.ChangeToFolderEnabled == false then return false end if not config.From or not config.To then warn("[Change] Missing RebirthConfig.From/RebirthConfig.To") return true end Farm.changingFolder = true Farm.stopMovement() Farm.setStatus("Đang chuyển folder vì cash dưới $50K") task.wait(tonumber(config.ChangeDelay) or 3) for _ = 1, 30 do if getgenv().client then break end task.wait(0.5) end local client = getgenv().client config = getgenv().RebirthConfig or {} local currentCash = Farm.getCash() if not Farm.isCurrentRun() or config.ChangeToFolderEnabled == false or not Farm.cashValue or not Farm.cashValue.Parent or currentCash >= Farm.LOW_CASH_FOLDER_THRESHOLD then Farm.changingFolder = false return not Farm.isCurrentRun() end if not config.From or not config.To then Farm.changingFolder = false warn("[Change] Missing RebirthConfig.From/RebirthConfig.To") return true end if not client then Farm.changingFolder = false warn("[Change] getgenv().client chưa sẵn sàng") return true end local completed, ok, result = false, false, nil local callThread = task.spawn(function() ok, result = pcall(function() return client:ChangeToFolder(config.From, config.To, config.ChangeWithoutReplace == true, config.ConfigID) end) completed = true end) local deadline = tick() + Farm.CHANGE_FOLDER_TIMEOUT while not completed and tick() < deadline do task.wait(0.1) end Farm.changingFolder = false if not completed then Farm.changeFolderTimedOut = true pcall(task.cancel, callThread) warn("[Change] ChangeToFolder timed out; refusing a duplicate call") return true end if ok then Farm.changedFolder = true print("[Change] ChangeToFolder called because cash is below $50K: " .. tostring(result)) else warn("[Change] ChangeToFolder failed: " .. tostring(result)) end return true end function Farm.runIteration() if not Farm.isCurrentRun() then Farm.stoppedReason = "Runtime cũ đã dừng" return end if Action.changeToFolderIfLowCash() then return end Fleet.tickPresence() if Fleet.checking then Farm.stopMovement() Farm.setStatus("Đang kiểm tra fleet") task.wait(0.2) return end if Fleet.hopping then Farm.stopMovement() Farm.setStatus("Đang hop tránh account trùng") task.wait(0.2) return end local playerCount = #Players:GetPlayers() local minServerPlayers = math.max( 0, math.floor(tonumber((getgenv().CarpetFarmConfig or {}).MinServerPlayers) or 3) ) if playerCount < minServerPlayers then warn(("Fleet trigger hop population: %d/%d players"):format(playerCount, minServerPlayers)) Fleet.hop("population") return end local richPlayer, richCash = Fleet.findRichPlayer() if richPlayer then warn(("Fleet trigger hop rich player: $%.0f"):format(richCash)) Fleet.hop("rich") return end local plot = Farm.findMyPlot() if not plot then Farm.campMode = false Farm.setStatus("Đang tìm base") task.wait(0.5) return end local snapshot = Farm.snapshot() Farm.setCampMode(plot, snapshot) if Action.buySpeedCoilIfReady(plot) then return end if snapshot.plot ~= plot or not Farm.farmReady(plot) then Farm.setStatus("Đang đồng bộ dữ liệu") return end if tick() < Farm.actionRetryAt then Farm.setStatus("Đang chờ đồng bộ action") task.wait(0.25) return end if snapshot.plot ~= plot or not snapshot.incomeKnown or not snapshot.allResolved then Farm.dataSyncSince = Farm.dataSyncSince or tick() if tick() - Farm.dataSyncSince >= Farm.DATA_SYNC_TIMEOUT then Farm.dataSyncSince = tick() warn("Dữ liệu base chưa đồng bộ; tiếp tục chờ thay vì hop.") end Farm.setStatus("Đang chờ dữ liệu base") task.wait(0.25) return end Farm.dataSyncSince = nil local campOccupied = Farm.campCounts(snapshot) if Farm.rangeEmpty(snapshot, 1, 10) then Farm.cleanupActive = false Farm.cleanupDeadline = 0 Farm.cleanupRetryAt = 0 elseif Farm.cleanupActive or campOccupied >= Farm.CAMP_SELL_THRESHOLD then local now = tick() if not Farm.cleanupActive and now >= Farm.cleanupRetryAt then Farm.cleanupActive = true Farm.cleanupDeadline = now + Farm.CLEANUP_TIMEOUT end if Farm.cleanupActive and now >= Farm.cleanupDeadline then Farm.cleanupActive = false Farm.cleanupRetryAt = now + Farm.CLEANUP_RETRY warn("Bán pet camp quá hạn; tạm nghỉ rồi thử lại.") end if not Farm.cleanupActive then Farm.setStatus("Đang chờ thử bán lại toàn bộ pet") return end Farm.setStatus(("Đang bán toàn bộ pet (%d/10)"):format(campOccupied)) Action.cleanup(snapshot, 1, 10) return end Farm.setStatus(("Đang quét Gangster (%d/%d slot)"):format(campOccupied, Farm.CAMP_SELL_THRESHOLD)) local candidate = Farm.selectPrompt() if candidate then Farm.setStatus("Đang tranh " .. candidate.item.name) Action.buy(candidate.item, candidate.prompt) return end if Farm.hasLiveContestPrompt(snapshot) then Farm.stopMovement() Farm.setStatus("Đang chờ lượt tranh Gangster") task.wait(0.1) return end local lockConfig = getgenv().RebirthConfig or {} local lockRetry = math.clamp(tonumber(lockConfig.LockRetrySec) or Farm.LOCK_RETRY, 0.5, 10) if lockConfig.LockBase ~= false and Farm.getLockState(plot) ~= true and tick() - Farm.lastLockAttempt >= lockRetry then Farm.lastLockAttempt = tick() Farm.setStatus("Đang khóa base; tranh mua được ưu tiên") local locked, lockCandidate = Action.lockBase(plot) if lockCandidate then Farm.setStatus("Đang tranh " .. lockCandidate.item.name) Action.buy(lockCandidate.item, lockCandidate.prompt) return end if not locked then Farm.setStatus("Chưa khóa được base; sẽ thử lại") end return end Farm.setStatus("Đang quay về đầu Carpet") local _, interruptedCandidate = Action.returnToCarpetHead(plot, true) if interruptedCandidate then Farm.setStatus("Đang tranh " .. interruptedCandidate.item.name) Action.buy(interruptedCandidate.item, interruptedCandidate.prompt) return end Farm.setStatus("Đang camp chờ Gangster") end function Farm.bindWorldSignals() local env = getgenv() local previous = env.__sabCarpetWorldConnections if type(previous) == "table" then for _, connection in ipairs(previous) do pcall(connection.Disconnect, connection) end end local connections = {} local ok, problem = pcall(function() connections[#connections + 1] = Workspace.DescendantAdded:Connect(function(object) if object.Name == "Carpet" and object:IsA("BasePart") then Farm.carpetCacheDirty = true end end) connections[#connections + 1] = Workspace.DescendantRemoving:Connect(function(object) if object.Name == "Carpet" and object:IsA("BasePart") then Farm.carpetCacheDirty = true end end) connections[#connections + 1] = CollectionService:GetInstanceAddedSignal("Animal"):Connect(function(animal) Farm.watchAnimal(animal) Farm.promptCacheDirty = true end) connections[#connections + 1] = CollectionService:GetInstanceRemovedSignal("Animal"):Connect(function() Farm.promptCacheDirty = true end) end) if not ok then for _, connection in ipairs(connections) do pcall(connection.Disconnect, connection) end error(problem) end Farm.worldConnections = connections env.__sabCarpetWorldConnections = connections end function Farm.cleanupRuntime() local worldConnections = Farm.worldConnections for _, connection in ipairs(worldConnections) do pcall(connection.Disconnect, connection) end Farm.worldConnections = {} if GlobalEnv.__sabCarpetWorldConnections == worldConnections then GlobalEnv.__sabCarpetWorldConnections = nil end for animal, connections in pairs(Farm.watchedAnimals) do for _, connection in ipairs(connections) do pcall(connection.Disconnect, connection) end Farm.watchedAnimals[animal] = nil end Farm.disconnectPodiumSignals() if Farm.cashConnection then pcall(Farm.cashConnection.Disconnect, Farm.cashConnection) end Farm.cashConnection = nil Farm.cashValue = nil Farm.cachedCash = nil Farm.leaderstats = nil if Farm.characterAddedConnection then pcall(Farm.characterAddedConnection.Disconnect, Farm.characterAddedConnection) end Farm.characterAddedConnection = nil Farm.bindMovementCharacter(nil) Farm.disconnectPlotSignals() for _, connection in ipairs(Farm.plotsFolderConnections) do pcall(connection.Disconnect, connection) end Farm.plotsFolderConnections = {} Farm.plotsFolder = nil Farm.plot = nil for _, connection in ipairs(Fleet.teleportConnections) do pcall(connection.Disconnect, connection) end Fleet.teleportConnections = {} Farm.snapshotCache = nil Farm.promptCache = {} Farm.carpetCache = {} Farm.identityPromptCache = setmetatable({}, { __mode = "k" }) Farm.reservations = {} Farm.contestServed = {} Farm.promptRetryAt = {} end local startupDeadline = tick() + 120 while (not game:IsLoaded() or not Players.LocalPlayer) and tick() < startupDeadline do task.wait(0.25) end Farm.player = Players.LocalPlayer Farm.bindMovementPlayer() if Farm.player then Farm.installStatusUi() local trackEnabled = getgenv().CarpetFarmConfig.HobeoTrackEnabled == true if not trackEnabled then GlobalEnv.HOBEO_HEARTBEAT_RUNNING = false GlobalEnv.__sabCarpetTrackerStarted = nil if type(_G) == "table" then _G.SAB_RUNNING = false end elseif not GlobalEnv.__sabCarpetTrackerStarted then if type(GlobalEnv.HOBEO_API_KEY) ~= "string" or GlobalEnv.HOBEO_API_KEY == "" then warn("HobeoTrack bật nhưng thiếu HOBEO_API_KEY; bỏ qua loader.") else GlobalEnv.__sabCarpetTrackerStarted = true pcall(function() loadstring(game:HttpGet("https://files.hobeotrack.com/Loader.lua"))() end) end end end if Farm.player and getgenv().CarpetFarmConfig.SelfTest == true then assert(Fleet.PLACE_ID == 96342491571673, "fleet place lock") assert(Fleet.USERNAME_CACHE_TTL == 24 * 60 * 60, "fleet username cache TTL") assert(Farm.CLEANUP_TIMEOUT == 60 and Farm.DATA_SYNC_TIMEOUT == 60, "bounded recovery waits") local parsedRegistry, parsedCount = Fleet.usernamesToSet({ "FleetUser", { username = "Other" }, "FleetUser", "bad name" }) assert(parsedCount == 2 and parsedRegistry.fleetuser and parsedRegistry.other, "fleet username parsing") local registryTest = { fleetuser = true } assert( Fleet.playerInRegistry({ UserId = Farm.player.UserId + 1, Name = "FleetUser" }, registryTest), "fleet username detection" ) assert( not Fleet.playerInRegistry( { UserId = Farm.player.UserId + 1, Name = "Other", DisplayName = "FleetUser" }, registryTest ), "fleet detection ignores display-name collisions" ) assert( not Fleet.playerInRegistry({ UserId = Farm.player.UserId, Name = "FleetUser" }, registryTest), "fleet detection excludes self" ) local campSlots = {} for index = 1, 10 do campSlots[index] = { state = "empty" } end local campSnapshot = { incomeKnown = true, allResolved = true, income = 0, slots = campSlots } local reservationPrompt, baselineSlots = {}, {} for index = 1, 10 do baselineSlots[index] = Farm.slotIdentity(campSlots[index]) end Farm.reservations[reservationPrompt] = { name = "Gangster Footera", createdAt = 0, expiresAt = math.huge, seenInbound = false, contesting = false, baselineSlots = baselineSlots, } campSlots[7] = { state = "occupied", syncedIndex = "Gangster Footera" } Farm.reconcileReservations(campSnapshot) assert(Farm.reservations[reservationPrompt] == nil, "cross-slot camp reservation confirmation") assert(Farm.CONTEST_SLICE == 1 and Farm.CONTEST_RETRY == 0.25, "contest rotation") assert(Farm.CAMP_SELL_THRESHOLD == 8, "camp sell threshold") assert(Fleet.TELEPORT_WAIT == 6 and Fleet.HOBEO_HOP_ATTEMPTS == 12 and Fleet.HOBEO_RETRY_GAP == 0.5, "Hobeo retry policy") assert(Farm.LOCK_TIMEOUT == 6 and Farm.LOCK_RETRY == 2, "base lock pacing") assert(Fleet.tried == GlobalEnv.__sabCarpetTriedJobs, "session JobId cache") assert(Farm.WAIT_FPS == 5 and Farm.MOVE_FPS == 15, "fps states") assert(Farm.LOW_CASH_FOLDER_THRESHOLD == 50000 and Farm.CHANGE_FOLDER_TIMEOUT == 20, "low-cash folder change") assert(Farm.SHOP_TIMEOUT == 25 and Farm.SPEED_COIL_NAME == "Speed Coil", "Speed Coil config") assert( Farm.SHOP_PROMPT_DELAY == 1.5 and Farm.SHOP_BUY_DELAY == 2 and Farm.SHOP_EQUIP_DELAY == 1, "Speed Coil pacing" ) assert(Farm.getRequiredName("Trippi Troppi") == nil, "Trippi contest disabled") assert(Farm.getRequiredName("Gangster Footera") == "Gangster Footera", "Gangster contest enabled") assert(Farm.targetIsCurrent(Farm.player.Name) == true, "current-name target") assert(Farm.targetIsCurrent(Farm.player.UserId) == true, "current-id target") assert(Farm.targetIsCurrent(nil) == false, "nil target") assert(Farm.targetIsCurrent("__other_player__") == false, "other-player target") end if Farm.stoppedReason then Farm.stopMovement() elseif not game:IsLoaded() or not Farm.player then Farm.stoppedReason = "Game readiness timeout" elseif type(fireproximityprompt) ~= "function" then Farm.stoppedReason = "prompt API unavailable" else Farm.bindWorldSignals() local fleetOk, fleetProblem = pcall(Fleet.start) if not fleetOk then Fleet.currentJobId = nil Fleet.hopping = false Fleet.checking = false warn("Fleet startup lỗi; tiếp tục farm: " .. tostring(fleetProblem):sub(1, 300)) end Farm.markLoaded() while Farm.isCurrentRun() and not Farm.stoppedReason do local ok, problem = pcall(Farm.runIteration) if not ok then Farm.actionRetryAt = tick() + Farm.TRANSIENT_RETRY Farm.setStatus("Lỗi tạm thời, đang thử lại") warn("Carpet farm loop error; retrying: " .. tostring(problem):sub(1, 300)) task.wait(1) end task.wait(Farm.CORE_LOOP_DELAY) end end if Farm.stoppedReason then Farm.stopMovement() Farm.setStatus("Đã dừng: " .. Farm.stoppedReason) warn("Carpet farm stopped: " .. Farm.stoppedReason) end pcall(Farm.cleanupRuntime) if Farm.isCurrentRun() then GlobalEnv.__sabCarpetStop = nil GlobalEnv.__sabCarpetRunToken = nil end