--!strict -- Seraph AutoMine v2 — flat single-tick loop variant -- -- Alternative to the FSM-based `auto_mine.lua`. Same goal (auto-farm -- gemstones in MINEWORLD), different control shape: flat tick loop -- instead of state machine. -- -- Algorithm: -- 1. Loop with ping-aware dynamic delay (~280-450 ms + jitter) -- 2. Auto-enter MINEWORLD if not already in -- 3. Equip best available pickaxe once per session/world -- 4. Track per-tile hit attempts; mark dead-ends after 12 misses -- 5. Pick best target: closest collectable > closest gemstone (60-tile radius) -- 6. Skip targets near AI enemies (3-tile buffer) -- 7. Hit closest melee-range enemy first -- 8. Auto-collect drops within 2-tile magnet radius -- 9. Walk path one step at a time; mine adjacent block if path blocked -- -- Compared to existing FSM-based `auto_mine.lua`, this is a flat -- single-tick loop — easier to reason about, harder to misconfigure, -- but less recoverable from edge cases the FSM handles explicitly. -- -- Cancel: stop the script (getClient():scripting():stop()). ------------------------------------------------------------------ -- Constants ------------------------------------------------------------------ -- Pickaxe priority — best to worst local PICKAXE_PRIORITY = { 4195, -- WeaponPickaxeDark 4093, -- WeaponPickaxeEpic 4092, -- WeaponPickaxeMaster 4091, -- WeaponPickaxeHeavy 4090, -- WeaponPickaxeSturdy 4089, -- WeaponPickaxeBasic 4088, -- WeaponPickaxeFlimsy 4087, -- WeaponPickaxeCrappy } -- Gemstone block ID ranges (mineable, in walls — not pickup-drops) local function is_minegem(block_id) if not block_id then return false end return (block_id >= 3995 and block_id <= 4003) or (block_id >= 4101 and block_id <= 4102) or (block_id >= 4154 and block_id <= 4162) end -- Pathfinding masks (used to mark dead-ends/enemies as unwalkable virtually) local BEDROCK_BLOCK_ID = 3993 -- Tunables local MAX_TILE_ATTEMPTS = 12 -- per-tile hit retry cap before flagging dead-end local SEARCH_RADIUS = 60 -- tiles, gemstone hunt window local COLLECT_RADIUS = 2 -- tiles, magnet pickup local ENEMY_BUFFER = 3 -- tiles, skip targets near enemies local MELEE_RANGE = 2 -- tiles, max combat reach local BASE_DELAY_MS = 280 -- baseline tick (real-client ~250ms; +30 for safety) local JITTER_MAX_MS = 80 -- random jitter to avoid bot-shaped cadence local FALL_TICK_MS = 10 -- short tick while in mid-air physics local WORLD_TRANSITION_WAIT = 4.0 -- seconds, after sending mines() before re-evaluating ------------------------------------------------------------------ -- Helpers ------------------------------------------------------------------ local function now_ms() return os.time() * 1000 -- seconds-resolution; sufficient for cooldown comparison end local function distance_sq(ax, ay, bx, by) local dx, dy = ax - bx, ay - by return dx * dx + dy * dy end local function manhattan(ax, ay, bx, by) return math.abs(ax - bx) + math.abs(ay - by) end local function is_walkable_tile(client, x, y) -- Trust Seraph's walkability check (handles air/portal/door etc.) return client:isWalkable({ x = x, y = y }) or false end local function find_best_pickaxe(inventory) if not inventory then return nil end -- Build a quick lookup: inventory entry → block_id present local owned = {} for _, entry in ipairs(inventory) do if entry.amount and entry.amount > 0 then owned[entry.block_id or entry.blockId or entry.id] = true end end for _, pid in ipairs(PICKAXE_PRIORITY) do if owned[pid] then return pid end end return nil end local function any_enemy_near(enemies, tx, ty, buffer) for _, e in pairs(enemies) do if e.alive ~= false and e.map_x and e.map_y then local dsq = distance_sq(tx, ty, e.map_x, e.map_y) if dsq < (buffer * buffer) then return true end end end return false end local function find_closest_enemy_in_melee(enemies, px, py) local best, best_dist = nil, math.huge for _, e in pairs(enemies) do if e.alive ~= false and e.map_x and e.map_y and e.map_x ~= 0 then local d = manhattan(px, py, e.map_x, e.map_y) if d <= MELEE_RANGE and d < best_dist then best_dist = d best = e end end end return best end ------------------------------------------------------------------ -- Target selection -- -- Priority: -- 1. Nearest collectable not on cooldown, away from enemies → "collect" -- 2. If no collectable, nearest gemstone within radius → "mine" ------------------------------------------------------------------ local function pick_target(client, world, px, py, dead_ends, recent_collected) local enemies = world:enemies() or {} local collectables = world:collectables() or {} local best_target, best_dist_sq = nil, math.huge -- Pass 1: collectables for _, c in pairs(collectables) do local cid = c.collectable_id or c.id if cid and not recent_collected[cid] then local cx, cy = c.map_x, c.map_y if cx and cy and not any_enemy_near(enemies, cx, cy, ENEMY_BUFFER) then local d = distance_sq(px, py, cx, cy) if d < best_dist_sq then best_dist_sq = d best_target = { kind = "collect", id = cid, x = cx, y = cy, block_id = c.block_type or c.blockType, } end end end end -- Pass 2: gemstones (only if no collectable found) if not best_target then local size = world:size() local w_width = (size and size.width) or 0 local w_height = (size and size.height) or 0 local min_x = math.max(0, px - SEARCH_RADIUS) local max_x = math.min(w_width - 1, px + SEARCH_RADIUS) local min_y = math.max(0, py - SEARCH_RADIUS) local max_y = math.min(w_height - 1, py + SEARCH_RADIUS) for y = min_y, max_y do for x = min_x, max_x do local key = x .. "," .. y if not dead_ends[key] then local tile = world:tile({ x = x, y = y }) local block = tile and (tile.foreground or tile.fg or tile.id) if is_minegem(block) and not any_enemy_near(enemies, x, y, ENEMY_BUFFER) then local d = distance_sq(px, py, x, y) if d < best_dist_sq then best_dist_sq = d best_target = { kind = "mine", x = x, y = y, block_id = block } end end end end end end return best_target end ------------------------------------------------------------------ -- Main loop ------------------------------------------------------------------ function runAutomine(client, opts) client = client or getClient() if not client then error("runAutomine: no client") end opts = opts or {} local target_level = opts.level or 0 local target_world = (opts.world or "MINEWORLD"):upper() print(string.format("[automine] starting — world=%s level=%d", target_world, target_level)) -- Per-tile attempt counter; key is "x,y" local tile_attempts = {} -- { ["x,y"] = count } local dead_ends = {} -- { ["x,y"] = true } when count >= MAX_TILE_ATTEMPTS local recent_collected = {} -- { [cid] = expiry_ms } cooldown after collect local equipped_pickaxe = nil local current_world = nil local sticky_target = nil while true do -- ── 1. Status gate ────────────────────────────────────── if not client:isAlive() then print("[automine] client not alive — exiting") return end local status = client:status() or "" if status == "Idle" or status == "Disconnected" or status == "Error" then print("[automine] session " .. status .. " — exiting") return end if status == "Connecting" or status == "Authenticating" or status == "Redirecting" or status == "JoiningWorld" then task.wait(0.5) end -- ── 2. Ensure in MINEWORLD ────────────────────────────── local world_name = client:world() and (client:world().name or client:world().world_name) local in_target = world_name and world_name:upper() == target_world if not in_target then print("[automine] entering " .. target_world .. " level " .. target_level) client:mines(target_level) task.wait(WORLD_TRANSITION_WAIT) -- reset world-scoped state on transition tile_attempts = {} dead_ends = {} equipped_pickaxe = nil sticky_target = nil current_world = world_name -- continue → next loop iteration re-checks end if world_name ~= current_world then tile_attempts = {} dead_ends = {} equipped_pickaxe = nil sticky_target = nil current_world = world_name end -- ── 3. Equip pickaxe ───────────────────────────────────── if not equipped_pickaxe then local inv = client:inventory() local pid = find_best_pickaxe(inv) if pid then client:wear(pid) equipped_pickaxe = pid print(string.format("[automine] equipped pickaxe block=%d", pid)) else print("[automine] WARN no pickaxe in inventory — HB packets will be ignored") task.wait(2.0) end end -- ── 4. Compute dynamic tick delay ──────────────────────── local point = client:point() or { x = 0, y = 0 } local px, py = point.x or 0, point.y or 0 local cur_tile = client:isWalkable({ x = px, y = py }) local below_tile = client:isWalkable({ x = px, y = py + 1 }) local is_falling = cur_tile and below_tile local sleep_ms if is_falling then sleep_ms = FALL_TICK_MS else local ping = client:ping() or 0 local jitter = math.random(0, JITTER_MAX_MS) sleep_ms = BASE_DELAY_MS + jitter if ping > 150 then sleep_ms = sleep_ms + (ping - 100) end end -- ── 5. Sweep cooldown table ────────────────────────────── local cleanup_ts = now_ms() - 5000 for cid, expiry in pairs(recent_collected) do if expiry < cleanup_ts then recent_collected[cid] = nil end end -- ── 6. Combat: closest enemy in melee range ────────────── local enemies = client:world() and client:world().enemies and client:world():enemies() if not enemies then local w = client:world() if w and w.enemies then enemies = w:enemies() end end enemies = enemies or {} local target_enemy = find_closest_enemy_in_melee(enemies, px, py) if target_enemy then local id = target_enemy.ai_id or target_enemy.id client:hitEnemy(target_enemy.map_x, target_enemy.map_y, id) -- print(string.format("[automine] hit enemy id=%d at (%d,%d)", id, target_enemy.map_x, target_enemy.map_y)) end -- ── 7. Auto-collect drops in magnet radius ─────────────── local world = client:world() if world and world.collectables then local drops = world:collectables() or {} for _, c in pairs(drops) do local cid = c.collectable_id or c.id local cx, cy = c.map_x, c.map_y if cid and cx and cy and not recent_collected[cid] then if math.abs(cx - px) <= COLLECT_RADIUS and math.abs(cy - py) <= COLLECT_RADIUS then client:collect(cid) recent_collected[cid] = now_ms() + 3000 -- 3s cooldown end end end end -- ── 8. Pick + path-walk to best target ─────────────────── if world then -- Refresh sticky target validity local target = nil if sticky_target then local key = sticky_target.x .. "," .. sticky_target.y local still_valid = not dead_ends[key] if sticky_target.kind == "collect" then still_valid = still_valid and not recent_collected[sticky_target.id] -- best-effort: check collectable still exists local drops = (world.collectables and world:collectables()) or {} local found = false for _, c in pairs(drops) do if (c.collectable_id or c.id) == sticky_target.id then found = true; break end end still_valid = still_valid and found elseif sticky_target.kind == "mine" then local tile = world:tile({ x = sticky_target.x, y = sticky_target.y }) local block = tile and (tile.foreground or tile.fg or tile.id) still_valid = still_valid and is_minegem(block) end if still_valid then target = sticky_target end end if not target then target = pick_target(client, world, px, py, dead_ends, recent_collected) sticky_target = target end if target then local path = client:getPath({ x = target.x, y = target.y }) if path and #path >= 2 then local next_step = path[2] local is_last_step = #path == 2 local nx, ny = next_step.x, next_step.y local next_walkable = is_walkable_tile(client, nx, ny) -- Adjacent-collectable safety grab if target.kind == "collect" then local dx, dy = math.abs(target.x - px), math.abs(target.y - py) if dx <= 1 and dy <= 1 and not recent_collected[target.id] then client:collect(target.id) recent_collected[target.id] = now_ms() + 3000 end end if next_walkable and not (is_last_step and target.kind == "mine") then -- Walk one step client:setPoint({ x = nx, y = ny }) else -- Path blocked OR target itself is the block we want to mine local key = nx .. "," .. ny if dead_ends[key] then -- This blocking tile is a dead-end; mark target dead-end too dead_ends[(target.x .. "," .. target.y)] = true sticky_target = nil elseif nx == px and ny == py then -- Safety: never hit own tile print("[automine] WARN A* suggested hitting own tile — skipping") else client:hit({ x = nx, y = ny }) tile_attempts[key] = (tile_attempts[key] or 0) + 3 -- burst counts ~3 hits if tile_attempts[key] >= MAX_TILE_ATTEMPTS then dead_ends[key] = true if target.x == nx and target.y == ny then sticky_target = nil end print(string.format("[automine] dead-end (%d,%d) after %d attempts", nx, ny, tile_attempts[key])) end end end elseif path and #path == 1 then -- Already on target tile if target.kind == "collect" and not recent_collected[target.id] then client:collect(target.id) recent_collected[target.id] = now_ms() + 3000 end else -- No path → mark this target as unreachable dead_ends[(target.x .. "," .. target.y)] = true sticky_target = nil end end end -- ── 9. Sleep until next tick ───────────────────────────── task.wait(sleep_ms / 1000) end end -- Auto-run when dofile()'d directly if getClient and not _AUTOMINE_V2_LOADED then _AUTOMINE_V2_LOADED = true local ok, err = pcall(function() runAutomine(getClient()) end) if not ok then print("[automine] error: " .. tostring(err)) end end