All scripts

Coal/Mith/Addy Miner Mining guild

by SolemnVanguard ·Mining

RSCFalador

Coal/Mith/Addy Miner Mining guild

7

Installed

1.7

Version

25 Aug 2026

Updated

About this script

Coal/Mith/Addy Miner Mining guild with toggle for which ores you want. It will always priorit addy > Mith > Coal
Got sleeping and banking + mod protection

Be max zoomed out on your client, and face camera East.

Builds

VersionKindPublishedSizeWhat changed
1.7 current text script 24 Aug 2026 16 KB Fixed more pathing issues and door opening
1.2 text script 22 Aug 2026 12 KB Fixed better pathing between bank and ladder
1.1 text script 21 Aug 2026 13 KB First upload.

Source of 1.7

Published by the author. This is the current build, exactly as it will be delivered.

script "Coal Miner Guild"
author "SolemnVanguard"
version "1.7"
every 600

setting string depositIds = "155,160,159,158,154,153,157" "Comma-separated item ids to deposit at the bank (ores + uncut gems)"
setting int doorId = 2 "Wall id of the door between the bank chest and ladder 223 (0 = auto-detect the nearest closed door within 8 tiles instead of using doorId/doorTileX/doorTileY)"
setting int doorTileX = 275 "X of the door's tile - 'Door' (wall id 2) is reused all over the map, so doorId alone isn't unique; this pins it to the one confirmed instance"
setting int doorTileY = 562 "Y of the door's tile (confirmed door: id 2 at (275, 562))"

# Mines Adamantite rock (id 108, highest priority), then Mithril rock
# (id 107), then Coal rock (id 110 or 111) in the guild mine - each type
# has its own on/off toggle in the UI ("Mine adamantite", "Mine
# mithril", "Mine coal") so any combination can be enabled at once.
# Priority rocks are searched within 30 tiles regardless of how far that
# is compared to nearby coal. Rocks with tile.y <= 3387 are ignored
# entirely for all three types. When the inventory is full, finds the
# up-ladder (id 198) and interacts with it directly, then finds the
# Bank Chest (id 942) and interacts with it directly, deposits each
# item id from the depositIds setting (one per pass, 650ms apart,
# skipping any id the inventory doesn't actually contain), then finds
# the down-ladder (id 223) and interacts with it directly to resume
# mining. Uses Sleeping Bag (id 1263) at 100% fatigue.
#
# Fatigue failsafe: if fatigue jumps more than 25 points in a single
# pass, that is treated as suspicious (e.g. a mod forcing it up to test
# you) and the script halts (shows an orange warning) instead of using
# the Sleeping Bag as if it were normal. No teleport protection.
#
# Mining does not use the batch system, so batchActive() never fires for
# it. Instead, a swing is tracked as "in progress" until the server's own
# chat message reports its outcome (success, a scratch/miss, an empty
# rock, a bonus gem find, or "Nothing interesting happens" when a click
# lands but produces no real mining action), and only then does the
# script click again - this stops it from spamming clicks on the same
# rock mid-swing, and reacts to a rock depleting almost instantly since
# the very next pass re-evaluates which rock to click.
#
# Ladder crossings: no path system anymore - the up-ladder (id 198) and
# down-ladder (id 223) are each found and interacted with directly via
# nearestObject + reach() + interact(), the same proven pattern used for
# the Bank Chest.
#
# Unreachable/empty rocks: each rock tile gets its own cooldown entry
# (keyed by tile, in rockSkips) instead of one shared slot - so an empty
# adamantite rock and an unreachable mithril rock can both be on cooldown
# at once without one clobbering the other. reach() failing to reach
# DRAWN status on the same priority-ore tile 5 times in a row (e.g. it is
# boxed in by other rocks) puts that tile on cooldown for ~15s too, same
# as an emptied-out rock - each failed reach() can cost up to 7 seconds
# per the engine's own docs.
#
# Chat diagnostics: every server line can optionally be printed via the
# "Debug: print every chat line" toggle (off by default) - turn it on if
# mining ever seems to hang so a new/unhandled server message can be spotted.
#
# Doors: the door sits on the ladder-223/bank-chest side, so it gets crossed
# both ways each cycle - goingToMine checks for it before reaching ladder 223,
# and banking checks for it before reaching the chest (i.e. right after
# climbing up, since 223's landing spot is on that same side). Pinned by
# default to the confirmed door: wall id 2 ("Door") at tile (275, 562) -
# id alone isn't enough since "Door" is reused all over the map, so it's
# matched on id AND that exact tile (doorId/doorTileX/doorTileY settings).
# Set doorId to 0 to fall back to the old proximity guess (nearest closed
# door within 8 tiles offering "Open") if this ever needs to run somewhere
# else with a different door.

var miningActive = false
var miningActiveSince = 0
var warnedMiningStuck = false
var lastAttemptedTile = tile(0, 0)

var rockSkips = bagList()   # per-tile cooldowns/fail-streaks, keyed by str(tile)

var prevFatigueVal = -1
var haltedFatigueJump = false
var fatigueJumpLimit = 25

var depositIdList = range(0, 0)   # filled in on start from the setting
var depositIndex = 0

fun fatigueSuspiciousCheck() {
    let curFatigue = fatigue()
    if prevFatigueVal >= 0 {
        let jump = curFatigue - prevFatigueVal
        if jump > fatigueJumpLimit {
            print("Fatigue spike detected:", prevFatigueVal, "->", curFatigue, "- halting")
            haltedFatigueJump = true
        }
    }
    prevFatigueVal = curFatigue
}

fun rockIsSkipped(tile t) -> bool {
    let existing = findBag(rockSkips, "key", str(t))
    if existing == none { return false }
    return nowMillis() < bagNumberOr(existing, "until", 0)
}

fun skipRock(tile t, int ms) {
    let k = str(t)
    let existing = findBag(rockSkips, "key", k)
    if existing == none {
        let fresh = bagOf("key", k)
        bagPut(fresh, "until", nowMillis() + ms)
        rockSkips = append(rockSkips, fresh)
    } else {
        bagPut(existing, "until", nowMillis() + ms)
        bagPut(existing, "fails", 0)
    }
}

fun registerReachFail(tile t) -> int {
    let k = str(t)
    let existing = findBag(rockSkips, "key", k)
    if existing == none {
        let fresh = bagOf("key", k)
        bagPut(fresh, "fails", 1)
        rockSkips = append(rockSkips, fresh)
        return 1
    }
    bagInc(existing, "fails", 1)
    return bagNumberOr(existing, "fails", 0)
}

fun clearReachFails(tile t) {
    let existing = findBag(rockSkips, "key", str(t))
    if existing != none {
        bagPut(existing, "fails", 0)
    }
}

# True if a closed door was found and a step was taken toward/through it -
# call this before trying to reach the ladder; if it returns true, return
# for the pass instead of also trying the ladder (one action per pass).
# When doorId is set, this only ever considers walls of that id AND sitting
# exactly on (doorTileX, doorTileY) - "Door" is reused all over the map, so
# id alone isn't unique, but id + exact tile pins it to the one real door.
fun handleBlockingDoor() -> bool {
    if doorId > 0 {
        let target = tile(doorTileX, doorTileY)
        for w in walls(id: doorId, command: "Open", within: 30) {
            if w.tile == target {
                return openDoor(w)
            }
        }
        return false
    }
    let nearby = nearestWall(command: "Open", within: 8)
    if nearby == none { return false }
    return openDoor(nearby)
}

fun openDoor(wall d) -> bool {
    print("Opening door: id", d.id, "at", str(d.tile), "name", d.name)
    if reach(d) == "DRAWN" {
        interact(d, "Open")
    }
    return true
}

on message(line) {
    if uiToggled("debugChat") { print("MSG:", line) }
    if contains(line, "You only succeed in scratching the rock") {
        miningActive = false
    }
    if contains(line, "You manage to obtain some coal") {
        miningActive = false
    }
    if contains(line, "You manage to obtain some adamantite ore") {
        miningActive = false
    }
    if contains(line, "You manage to obtain some mithril ore") {
        miningActive = false
    }
    if contains(line, "You just found a ruby") {
        miningActive = false
    }
    if contains(line, "You just found a emerald") {
        miningActive = false
    }
    if contains(line, "You just found a sapphire") {
        miningActive = false
    }
    if contains(line, "You just found a diamond") {
        miningActive = false
    }
    if contains(line, "There is currently no ore available in this rock") {
        miningActive = false
        skipRock(lastAttemptedTile, 15000)   # avoid it for ~15s, then retry
    }
    if contains(line, "Nothing interesting happens") {
        # The click landed but produced no real mining action - treat it
        # as a failed attempt so the script tries again next pass instead
        # of hanging forever waiting for a swing outcome that never comes.
        miningActive = false
    }
}

on start {
    if not bankAvailable() {
        print("Bank not available")
    }

    uiToggle("mineAddy", "Mine adamantite", true)
    uiToggle("mineMith", "Mine mithril", true)
    uiToggle("mineCoal", "Mine coal", true)
    uiToggle("debugChat", "Debug: print every chat line", false)

    for piece in split(depositIds, ",") {
        let parsed = parseInt(piece)
        if parsed != none {
            depositIdList = append(depositIdList, parsed)
        }
    }
    print("Deposit ids loaded:", count(depositIdList))
}

state mining {
    fatigueSuspiciousCheck()
    if haltedFatigueJump { return 999999 }

    if isSleeping() {
        print("Sleeping, fatigue left to sleep off:", sleepFatigue())
        return 2000
    }
    if fatigue() >= 100 {
        print("Fatigue is 100 - using Sleeping Bag")
        let bag = invItem(1263)
        if bag != none {
            interact(bag, "Sleep")
        }
        return random(600, 900)
    }

    if invIsFull() { become goingToBank }

    if miningActive {
        let waited = nowMillis() - miningActiveSince
        if waited > 10000 {
            print("miningActive stuck for over 10s with no outcome message - forcing it clear")
            miningActive = false
        } else {
            if waited > 5000 and not warnedMiningStuck {
                print("miningActive has been true for", waited, "ms with no outcome yet - still waiting")
                warnedMiningStuck = true
            }
            return 300
        }
    }

    if uiToggled("mineAddy") {
        for candidate in objectsByDistance(within: 30) {
            if candidate.id == 108 and candidate.tile.y > 3387 {
                if rockIsSkipped(candidate.tile) {
                    continue
                }
                let howReach = reach(candidate)
                if howReach == "DRAWN" {
                    lastAttemptedTile = candidate.tile
                    clearReachFails(candidate.tile)
                    if interact(candidate, "Mine") {
                        miningActive = true
                        miningActiveSince = nowMillis()
                        warnedMiningStuck = false
                    }
                } else {
                    print("Walking to adamantite rock at", str(candidate.tile), "- reach() =", howReach)
                    let fails = registerReachFail(candidate.tile)
                    if fails >= 5 {
                        print("Adamantite rock unreachable - skipping it for a while")
                        skipRock(candidate.tile, 15000)
                    }
                }
                return random(300, 600)
            }
        }
    }

    if uiToggled("mineMith") {
        for candidate in objectsByDistance(within: 30) {
            if candidate.id == 107 and candidate.tile.y > 3387 {
                if rockIsSkipped(candidate.tile) {
                    continue
                }
                let howReach = reach(candidate)
                if howReach == "DRAWN" {
                    lastAttemptedTile = candidate.tile
                    clearReachFails(candidate.tile)
                    if interact(candidate, "Mine") {
                        miningActive = true
                        miningActiveSince = nowMillis()
                        warnedMiningStuck = false
                    }
                } else {
                    print("Walking to mithril rock at", str(candidate.tile), "- reach() =", howReach)
                    let fails = registerReachFail(candidate.tile)
                    if fails >= 5 {
                        print("Mithril rock unreachable - skipping it for a while")
                        skipRock(candidate.tile, 15000)
                    }
                }
                return random(300, 600)
            }
        }
    }

    if uiToggled("mineCoal") {
        for rock in objectsByDistance(within: 15) {
            if (rock.id == 110 or rock.id == 111) and rock.tile.y > 3387 {
                if rockIsSkipped(rock.tile) {
                    continue
                }
                if reach(rock) == "DRAWN" {
                    lastAttemptedTile = rock.tile
                    if interact(rock, "Mine") {
                        miningActive = true
                        miningActiveSince = nowMillis()
                        warnedMiningStuck = false
                    }
                }
                return random(300, 600)
            }
        }
    }

    print("No coal rock found nearby - waiting")
    return 1000
}

state goingToBank {
    fatigueSuspiciousCheck()
    if haltedFatigueJump { return 999999 }

    if isSleeping() {
        print("Sleeping, fatigue left to sleep off:", sleepFatigue())
        return 2000
    }
    if fatigue() >= 100 {
        print("Fatigue is 100 - using Sleeping Bag")
        let bag = invItem(1263)
        if bag != none {
            interact(bag, "Sleep")
        }
        return random(600, 900)
    }

    if handleBlockingDoor() { return random(400, 700) }

    let ladder = nearestObject(id: 198, within: 40)
    if ladder == none {
        print("No up-ladder (198) found nearby - waiting")
        return 1000
    }

    if reach(ladder) == "DRAWN" {
        if interact(ladder, "Climb-up") {
            become banking
        }
    }
    return random(900, 1400)
}

state banking {
    fatigueSuspiciousCheck()
    if haltedFatigueJump { return 999999 }

    if isSleeping() {
        print("Sleeping, fatigue left to sleep off:", sleepFatigue())
        return 2000
    }
    if fatigue() >= 100 {
        if bankIsOpen() {
            bankClose()
            return 600
        }
        print("Fatigue is 100 - using Sleeping Bag")
        let bag = invItem(1263)
        if bag != none {
            interact(bag, "Sleep")
        }
        return random(600, 900)
    }

    if not bankIsOpen() {
        if handleBlockingDoor() { return random(400, 700) }

        let chest = nearestObject(id: 942, within: 20)
        if chest == none {
            print("No Bank Chest (942) found within 20 tiles")
            return 1000
        }

        if reach(chest) == "DRAWN" {
            interact(chest, "open")
            wait until bankIsOpen() timeout 2000 else { return 300 }
        }
        return 1000
    }

    let id = depositIdList[depositIndex]
    if id == none {
        depositIndex = 0
        bankClose()
        become goingToMine
    } else {
        if invContains(id) {
            bankDepositAll(id)
        }
        depositIndex = depositIndex + 1
        return 650
    }
}

state goingToMine {
    fatigueSuspiciousCheck()
    if haltedFatigueJump { return 999999 }

    if isSleeping() {
        print("Sleeping, fatigue left to sleep off:", sleepFatigue())
        return 2000
    }
    if fatigue() >= 100 {
        print("Fatigue is 100 - using Sleeping Bag")
        let bag = invItem(1263)
        if bag != none {
            interact(bag, "Sleep")
        }
        return random(600, 900)
    }

    if handleBlockingDoor() { return random(400, 700) }

    let ladder = nearestObject(id: 223, within: 40)
    if ladder == none {
        print("No down-ladder (223) found nearby - waiting")
        return 1000
    }

    if reach(ladder) == "DRAWN" {
        if interact(ladder, "Climb-down") {
            miningActive = false
            become mining
        }
    }
    return random(900, 1400)
}

on render {
    if haltedFatigueJump {
        let flashOn = wrap(nowMillis(), 0, 999) < 500
        let bg = "#cc6600ee"
        if not flashOn { bg = "#663300ee" }
        paintRoundFill(4, 300, 220, 60, 12, bg)
        paintTextCenteredSized(114, 322, "FATIGUE SPIKE", "white", 16)
        paintTextCenteredSized(114, 344, "SCRIPT STOPPED", "white", 12)
    }
}