Polymorphic 122 Posted June 30, 2023 Share Posted June 30, 2023 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! Quote Link to comment Share on other sites More sharing options...
WrEcked 2 Posted July 10 Share Posted July 10 Thanks for the great demonstration :) Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.