Jump to content

Understanding Dependency Injection through Woodcutting


Polymorphic

Recommended Posts

I will be taking a dive into the concept of Dependency Injection (DI) using the TribotRoutine class in a woodcutting script. DI is a software design pattern that is used to make software more modular and maintainable. It refers to the way of creating and assigning dependencies (objects/services your class needs to operate) to a class from outside.

Our Starting Point

Consider the class TribotRoutine, which is an abstract class containing the blueprint for running a woodcutting routine. It includes various components like bankTaskManager, loginManager, serverManager, consumableManager, foodManager, and threadExecutionManager which are initialized through the constructor.

In this code, we are making use of DI by passing the required dependencies through the constructor instead of creating them inside the class. This is a prime example of DI as it allows us to pass different implementations of these components, making the class more flexible. For example, we can pass a mock version of the bankTaskManager for testing.

abstract class TribotRoutine(
    final override val name: String,
    override val metrics: Metrics,
    override val failurePolicy: FailurePolicy,
    override var status: RoutineStatus,
    protected var preferredBankTask: BankTask? = null,
    protected val bankTaskManager: BankTaskManager,
    protected val loginManager: LoginManager,
    protected val serverManager: ServerManager,
    protected val consumableManager: ConsumableManager? = null,
    protected val foodManager: FoodManager? = null,
    protected val threadExecutionManager: ThreadExecutionManager,
) : Routine {
    protected val logger = Logger(name)

    protected val scriptPaint = ScriptPaint()

    protected val commercialScriptPaint = ScriptPaint(
        slogan = false,
        runtime = false,
        location = PaintLocation.TOP_LEFT_VIEWPORT,
        tagLineFont = Font("Segoe UI", Font.PLAIN, 13)
    )

    protected var setupPaint: BasicPaintTemplate? = commercialScriptPaint.templateBuilder
        .row(
            commercialScriptPaint.tagLine.toBuilder()
                .label { "Setting up" }
                .value { "Check out my other scripts!" }
                .build()
        )
        .row(
            commercialScriptPaint.tagLine.toBuilder()
                .label { "Paragon Wintertodt" }
                .build()
        )
        .row(
            commercialScriptPaint.tagLine.toBuilder()
                .label { "Angler's Choice Fishing Trawler" }
                .build()
        )
        .row(
            commercialScriptPaint.tagLine.toBuilder()
                .label { "And much more!" }
                .build()
        )
        .build()

    protected var cleanupPaint: BasicPaintTemplate? = commercialScriptPaint.templateBuilder
        .row(
            commercialScriptPaint.tagLine.toBuilder()
                .label { "Cleaning up" }
                .value { "Check out my other scripts!" }
                .build()
        )
        .row(
            commercialScriptPaint.tagLine.toBuilder()
                .label { "Paragon Wintertodt" }
                .build()
        )
        .row(
            commercialScriptPaint.tagLine.toBuilder()
                .label { "Angler's Choice Fishing Trawler" }
                .build()
        )
        .row(
            commercialScriptPaint.tagLine.toBuilder()
                .label { "And much more!" }
                .build()
        )
        .build()

    protected fun addSetupPaint() {
        Painting.addPaint {
            val toPaint = setupPaint ?: return@addPaint
            toPaint.render(it)
        }
    }

    protected fun removeSetupPaint() {
        setupPaint = null
    }

    protected fun addCleanUpPaint() {
        Painting.addPaint {
            val toPaint = cleanupPaint ?: return@addPaint
            toPaint.render(it)
        }
    }

    protected fun removeCleanupPaint() {
        cleanupPaint = null
    }

    override fun execute(
        context: RoutineContext,
        stop: List<RoutineStopCondition>
    ): RoutineStatus {
        var tick = 0

        while (true) {
            Waiting.wait(100)

            ++tick

            if (tick >= 100) {
                logger.debug("Updating all metrics and pipeline context")
                metrics.updateAll()
                updateRoutineScopeContext(context)
                tick = 0
            }

            if (stop.any { it.check() }) {
                logger.debug("Stop condition: true")
                return RoutineStatus.SUCCESS
            }

            if (failurePolicy.exceededMaxAttempts()) {
                logger.error("Too many failed attempts, shutting down")
                return RoutineStatus.FAILURE
            }

            if (!loginManager.login()) {
                logger.error("Failed to login, shutting down")
                return RoutineStatus.FAILURE
            }

            consumableManager?.checkAll()
                ?.let { metrics.incrementConsumed(it) }

            foodManager?.checkAll()
                ?.let { metrics.incrementAte(it) }

            status = executeLogic()

            when (status) {
                RoutineStatus.SUCCESS -> {
                    failurePolicy.resetFailureCount()
                    metrics.incrementSuccessCount()
                }

                RoutineStatus.FAILURE -> {
                    logger.debug("Failure policy unsuccessful")
                    failurePolicy.incrementFailure()
                    metrics.incrementFailureCount()
                }

                RoutineStatus.KILL -> {
                    logger.debug("Awaiting termination")
                    metrics.incrementKillCount()
                    break
                }

                else -> onRoutineStatus(status)
            }
        }

        return status
    }

    abstract fun executeLogic(): RoutineStatus

    open fun onRoutineStatus(status: RoutineStatus) {
        // Do nothing by default, left open, can be overridden by subclasses
    }
}

Running the Routine

The execute method of TribotRoutine runs the routine and keeps track of various statistics like the number of successful and failed attempts, the number of items consumed, etc.

The executeLogic method, which is abstract, is where the actual routine logic would go in a subclass of TribotRoutine. The status of the routine execution (SUCCESS, FAILURE, KILL) is updated depending on the result of this logic.

Injecting Dependencies

As we discussed earlier, the constructor of TribotRoutine accepts various parameters, which are its dependencies. These are passed into the class from outside when a new instance of the class is created. This is known as constructor injection, one of the most common forms of DI.

For example, when you create a new instance of a subclass of TribotRoutine, you might write something like this:

RegularChoppingRoutine(
	name = routineData.routineType.typeName,
	metrics = Metrics(),
	failurePolicy = DefaultFailurePolicy(maxFailAttempts = 50),
	status = RoutineStatus.FAILURE,
	woodcuttingData = woodcuttingData,
	automaticAxe = AutomaticAxe(),
	loginManager = LoginManager(DefaultFailurePolicy()),
	serverManager = ServerManager(),
	consumableManager = null,
	foodManager = null,
	preferredBankTask = null,
	bankTaskManager = BankTaskManager(),
	depositBoxAction = DepositBoxAction(),
	axeManager = AxeManager(),
	threadExecutionManager = ThreadExecutionManager(
		hitsplats = false,
		inventoryItems = true,
		trees = true,
		treeToTrack = woodcuttingData.treeType.tree
	)
)

Now, let's go over the RegularChoppingRoutine class to see how DI is implemented in it.

/**
 * The class acts as a Dependency Injection controller, orchestrating the interactions between various
 * dependencies like `BankTaskManager`, `LoginManager`, `ServerManager`, etc.
 */
class RegularChoppingRoutine(
    name: String,
    metrics: Metrics,
    failurePolicy: FailurePolicy,
    status: RoutineStatus,
    woodcuttingData: WoodcuttingData,
    automaticAxe: AutomaticAxe,
    loginManager: LoginManager,
    serverManager: ServerManager,
    consumableManager: ConsumableManager?,
    foodManager: FoodManager?,
    bankTaskManager: BankTaskManager,
    threadExecutionManager: ThreadExecutionManager,
    preferredBankTask: BankTask? = null,

    private val axeManager: AxeManager,
    private val depositBoxAction: DepositBoxAction
) : TribotWoodcuttingRoutine(
    name,
    metrics,
    failurePolicy,
    status,
    preferredBankTask,
    bankTaskManager,
    loginManager,
    serverManager,
    consumableManager,
    foodManager,
    threadExecutionManager,
    woodcuttingData,
    automaticAxe,
) {
    override fun configureTribot(
        setAntibanEnabled: Boolean,
        setMouseSpeed: Int
    ) {
        super.configureTribot(true, TribotRandom.normal(250, 25))
    }

    override fun executeLogic() = executeRegularChopping()

    override fun setup(context: RoutineContext): RoutineStatus {
        addSetupPaint()

        Painting.addPaint {
            val tree = threadExecutionManager.treeTracker.getNextTreeObject() ?: return@addPaint
            it.color = Color(0, 0,0)

            tree.model.ifPresent { model ->
                model.bounds.ifPresent { bounds ->
                    it.draw(bounds)
                }
            }
        }

        Painting.addPaint {
            val nexTile = threadExecutionManager.treeTracker.getNextSpawnTile() ?: return@addPaint
            it.color = Color(0, 0,0)

            nexTile.bounds.ifPresent { bounds -> it.draw(bounds) }
        }

        if (!loginManager.login()) {
            return RoutineStatus.KILL
        }

        setupInitialBankTask()

        // Cleanup will actually clear all paint listeners at the end of the routine execution
        setupPaint = null

        addMainPaint()

        threadExecutionManager.launchTasks()

        return RoutineStatus.SUCCESS
    }

    private fun setupInitialBankTask(): RoutineStatus {
        val builder = BankTask.builder()
        val axeData = woodcuttingData.axeData

        if (axeData.automatic) {
            if (!walkToAndOpenBank()) {
                return RoutineStatus.FAILURE
            }
            else {
                axeManager.changeAxes(automaticAxe)
                val bestAxe = axeManager.currentAxe
                if (bestAxe == null) {
                    return RoutineStatus.FAILURE
                }
                else {
                    if (bestAxe.canWield()) {
                        builder.addEquipmentItem(
                            EquipmentReq.slot(Equipment.Slot.WEAPON)
                                .item(
                                    bestAxe.itemId, Amount.of(1)
                                )
                        )
                    }
                    else {
                        builder.addInvItem(bestAxe.itemId, Amount.of(1))
                    }
                }
            }
        }
        else {
            if (axeData.wieldAxe) {
                builder.addEquipmentItem(
                    EquipmentReq.slot(Equipment.Slot.WEAPON)
                        .item(axeData.axeId, Amount.of(1))
                )
            }
            else {
                builder.addInvItem(axeData.axeId, Amount.of(1))
            }
        }

        preferredBankTask = builder.build()

        return RoutineStatus.SUCCESS
    }

    private fun updateBankTaskAndAxeManager() = condition {
        axeManager.changeAxes(automaticAxe)
        val newAxe = axeManager.currentAxe ?: return RoutineStatus.FAILURE
        val builder = BankTask.builder()

        if (newAxe.canWield()) {
            builder.addEquipmentItem(
                EquipmentReq.slot(Equipment.Slot.WEAPON)
                    .item(newAxe.itemId, Amount.of(1))
            )
        }
        else {
            builder.addInvItem(newAxe.itemId, Amount.of(1))
        }

        preferredBankTask = builder.build()

        return RoutineStatus.SUCCESS
    }

    private fun executeRegularChopping(): RoutineStatus {
        return when {
            Antiban.shouldTurnOnRun() && !Options.isRunEnabled() -> condition { Options.setRunEnabled(true) }

            woodcuttingData.axeData.automatic && axeManager.shouldChangeAxes(automaticAxe) -> {
                updateBankTaskAndAxeManager()
            }

            preferredBankTask?.let { !it.isSatisfied() } == true && !axeManager.hasCurrentAxe() -> {
                val taskRes = condition {
                    bankTaskManager.executeUnsatisfiedTask(
                        preferredBankTask!!,
                        woodcuttingData.treeType.tree.bankPosition
                    )
                }

                if (taskRes === RoutineStatus.FAILURE && Equipment.Slot.WEAPON.item.isPresent) {
                    if (BankEquipment.open() && BankEquipment.bankItem(Equipment.Slot.WEAPON)) {
                        Waiting.wait(600)
                    }
                }

                return taskRes
            }

            woodcuttingData.logDisposal === LogDisposal.DROP && treeAction.shouldDropLogs() -> {
                condition { treeAction.dropLogs() > 0 }
            }

            woodcuttingData.logDisposal === LogDisposal.BANK && treeAction.shouldBankLogs() -> {
                condition {
                    val closestPos = woodcuttingData.treeType.tree.getClosestBankPosition()
                    if (closestPos == woodcuttingData.treeType.tree.depositBoxPosition) {
                        // Now I would create a DepositBoxTask class, similar to the BankTask, and a DepositBoxManager,
                        // just like the BankTaskManager class.
                        if (!depositBoxAction.isOpen()) {
                            if (!depositBoxAction.isNearbyAndReachable() && !walkToBank(closestPos)) {
                                return@condition false
                            }
                            if (!depositBoxAction.ensureOpen()) {
                                return@condition false
                            }
                        }
                        val currentAxe = axeManager.currentAxe?.itemId ?: return@condition depositBoxAction.depositInventory()
                        if (Equipment.contains(currentAxe)) {
                            return@condition depositBoxAction.depositInventory()
                        }
                        val toBank = Inventory.getAll()
                            .map { it.id }
                            .filterNot { it == currentAxe }
                            .toIntArray()
                        return@condition depositBoxAction.depositAll(*toBank)
                    }

                    bankTaskManager.executeForceTask(
                        bankTask = preferredBankTask!!,
                        bankTile = closestPos
                    )
                }
            }

            // if not at tree area -> walk to it
            !treeAction.isInCurrentArea() -> {
                condition { treeAction.moveToCurrentArea() }
            }

            // if special att -> use axe special
            axeManager.shouldUseSpecial() -> condition { axeManager.activateSpecial() }

            // if chopping -> return success
            treeAction.isChopping() -> RoutineStatus.SUCCESS

            // if next tree found alive -> chop it
            threadExecutionManager.treeTracker.getNextTreeObject() !== null -> {
                when (woodcuttingData.treeType) {
                    TreeType.YEW_TREE_AT_EDGEVILLE -> {
                        return condition {
                            threadExecutionManager.treeTracker
                                .getNextTreeObject()
                                ?.let { gameObject ->
                                    if (gameObject.distance() > 10 && canReach(woodcuttingData.treeType.tree.bankPosition)) {
                                        val multiArea = woodcuttingData.treeType.tree.area
                                        val treeAreaFound =
                                            multiArea.areas.firstOrNull { area -> area.contains(gameObject) }
                                                ?: return@let false
                                        val walkingTile =
                                            treeAreaFound.allTiles.firstOrNull { tile -> tile.toLocalTile().isWalkable }
                                                ?: return@let false
                                        if (!preciseLocalWalkTo(walkingTile)) {
                                            return@let false
                                        }
                                    }
                                    treeAction.interactTreeObject(gameObject)
                                            && Waiting.waitUntil(getDynamicWaitTime(gameObject)) { treeAction.isChopping() }
                                } == true
                        }
                    }

                    else -> {
                        return condition {
                            threadExecutionManager.treeTracker
                                .getNextTreeObject()
                                ?.let {
                                    treeAction.interactTreeObject(it)
                                            && Waiting.waitUntil(getDynamicWaitTime(it)) { treeAction.isChopping() }
                                } == true
                        }
                    }
                }
            }

            // getNextSpawn not null -> wait at next respawn
            threadExecutionManager.treeTracker.getNextSpawnTile() !== null -> {
                condition {
                    threadExecutionManager.treeTracker
                        .getNextSpawnTile()
                        ?.let { spawnTile ->
                            if (spawnTile.distance() < 3) {
                                return@let true
                            }

                            val walkTile = woodcuttingData.treeType.tree.area.areas
                                .firstOrNull { it.contains(spawnTile) }
                                ?.allTiles
                                ?.filter { it.toLocalTile().isWalkable }
                                ?.randomOrNull() ?: return@let false

                            if (canReach(walkTile)) {
                                preciseLocalWalkTo(walkTile)
                            }
                            else {
                                globalWalkTo(walkTile)
                            }
                        } == true
                }
            }

            // if area done -> walk to next area
            threadExecutionManager.treeTracker.isAreaDone(treeAction.getCurrentArea()) -> {
                condition {
                    val nextArea = threadExecutionManager.treeTracker.getNextArea() ?: treeAction.tree.area.areas[0]
                    treeAction.moveToArea(nextArea)
                }
            }

            else -> {
                RoutineStatus.FAILURE
            }
        }
    }
}

This walkthrough is only a glimpse into the depth and utility of Dependency Injection. We've barely scratched the surface of this topic, and a deeper dive into the core concepts and applications would be beneficial for a complete understanding.

I am currently working on a more comprehensive tutorial that will provide an overarching view of the framework used in this example, and a more in-depth exploration of Dependency Injection. This tutorial will not only explain how each component works individually but will also show how they interact within the larger system, enabling us to see the full picture of how Dependency Injection can be used to create flexible and testable software.

Stay tuned for the upcoming tutorial, as it will undoubtedly enhance your understanding of Dependency Injection!

Link to comment
Share on other sites

  • 1 year later...

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...