Jump to content

A Cooks Tutorial [SDK Scripting Tutorial]


SkrrtNick

Recommended Posts

Introduction

I felt that Tribot needed a tutorial on how to write a script using the new SDK to assist those who may be trying to learn.

I'm not claiming to be an authority on the best way to do things, I am just hoping to help you get on the right track :) 

I've tried to capture as much as I can in this thread, but even still I have most definitely missed things - if there's anything you'd like to see added or if you have any questions please drop me a message or comment on this thread.

Cheers

 

Pre-Requisites:

A basic knowledge of java 

Unlike other clients, the Tribot SDK makes use of Optionals so it’s important you have a basic understanding of how Optionals work and how they are different to nullable objects. I recommend checking out this link if you need to get your head around them https://www.baeldung.com/java-optional

I can recommend using the oracle java tutorials at https://docs.oracle.com/javase/tutorial/ or doing the mooc.fi java programming course https://java-programming.mooc.fi/ 

Either Tribot VIP standard or premium

Intellij IDEA

Frameworks:

There are a number of scripting frameworks that have been open sourced and are available in the tutorial section of the forums, the options I am aware of are:

Task by Encoded
Node by Worthy
Decision Trees by Nullable
Behaviour Trees (kotlin) by Nullable

The frameworks vary in complexity and some are easier to grasp than others, in the context of beginning scripting I think you should look into each one and decide what is the easiest for you to begin with.

As this is a beginners tutorial, for the sake of simplicity I will be using Encoded’s Task framework.

Important links:
The Docs:

https://runeautomation.com/docs/sdk/kdocs/

The Gradle Template:

The Query System:

 

Setting up our environment:

Spoiler

1. Open IntelliJ IDEA 

image.png.3730608c979a0c38987580e202e979fc.png

2. Click File > New > Project from Version Control

image.png.4f4e30a011499553688b534f24bc5500.png

3. Paste the URL for the tribot script template in repository URL https://github.com/TribotRS/tribot-script-template

image.png.c9f447973755644966f5643d964da65b.png

4. Select the directory you want to have your environment setup in (it needs to be an empty directory)

image.png.4b4587b6e4aa08ff0d9c9a3efc3682c1.png

5. Click Clone

image.png.2eada85bf05a604dd6061919f156a512.png

6. Wait until the template finishes downloading and click Trust Project if prompted

image.png.802bfdc4dcdaea8ec77787bad29187dd.png

7. Once all background tasks finish running, click File > Project Structure 

image.png.8107b9a512b7cd2456bb76d109300bd7.png

8. On the Project tab, ensure a Java 11 JDK is selected or click the Project SDK dropdown* 

JQfDT3jRe5.gif.087a1ebe732722a7eb2caf49c618931c.gif

If you do not have a Java 11 JDK downloaded you can download it from this dropdown menu

image.png.16ac1852d2a8c91420f0f73f66c47bbc.png

image.png.3e26b53b5c91205f9c1856ae477719bc.png

9. Click Apply

image.png.8c7a8a642d9453f66c950469029dff75.png

10. Click OK to close the window

11. File > Settings

image.png.e10f1970a513627faece9dd1f1b9cf3d.png

12. Build, Execution,  Deployment

image.png.3b834a0f8b021bb7d591da604227f8bc.png

13. Gradle

image.png.9a8f8b143edcf81de86edfc02995bfd8.png

14. Ensure the Gradle JVM is set to a Java 11 JDK

image.png.8a7c6a902a0f3ceda0945d62e58e5bb4.png

15. Click OK to close the window

16. Build your project

image.png.70ccbfd0a1ff49f052879f37e0a94b60.png

Creating a library for our framework

Spoiler

1. Create a directory under libraries named however you like - I will be using "framework"

image.png.d9fe2e7b7ba14e4c42b52755b52f1539.png

2. Create a "src" directory under the newly created directory

image.png.ae94066991949f8a561cbba80e06e4fb.png

3. Create a "scripts" directory under the "src" directory

image.png.34c8cd554a8126cef68e03372dbae937.png

4. Open settings.gradle.kts

image.png.3bd710c00228cf156042f45c15e772bb.png

5. Add a line: "include("libraries:directoryName")" where directoryname is the name of the directory you created

image.png.48c13154d87565935b45af695f8db90a.png

6. Click the Load Gradle Changes icon

image.png.5aff00ba15d9f5615c16bcf0a7ebd8fb.png

7. Confirm it worked by looking at the directories you created

image.png.1eaa01e5d5a5869a9de2717353e214fa.png

8. Create the framework files under framework/src/scripts

image.png.e0146152957ffc617fba5e6a7f8cb9ad.png

Create a place for your new script

Spoiler

1. Copy the my-script file and paste it under scripts

image.png.660597dab5294b4a38e6537b389962fe.png

image.png.12184629b784b571cce773ac3aa42362.png

2. Create a name for your script, in this example I will use cooks-assistant

image.png.4516802617d6e74eb7460f4313d0cc44.png

3. Open settings.gradle.kts

image.png.b12598724dc110daa815766fc57073fe.png

4. Add the line include(scripts:scriptName) where scriptName is the name of your script

image.png.6330e0d3a09d44738d49401041a7e180.png

5. Click the Load Gradle Changes icon

image.png.5aff00ba15d9f5615c16bcf0a7ebd8fb.png

6. Confirm that it worked

image.png.8a80fef0f467e07dd0471c0f7f206721.png

Add the framework to your new script

Spoiler

1. Go into build.gradle.kts under your script's root directory

image.png.907e460b8c5deda3246b32f172d9de68.png

2. Add the line: "implementation(project(":libraries:libraryName"))" where libraryName is the name of your library

image.png.eaa05c913b4b9f50def80c40eaaff555.png

3. Click the Load Gradle Changes icon

image.png.5aff00ba15d9f5615c16bcf0a7ebd8fb.png

Planning the script

Spoiler

In the interest of doing as much as we can with this exercise, we will be gathering the required items instead of relying on the Grand Exchange for getting them.

It's important to plan a script before you start coding, this helps in reducing the amount of time you spend refactoring and rewriting down the track. 

Lets look at getting an egg:

flowchart.thumb.png.d94ef3844f3ad5b48fc8bb1bae012bb9.png

This should make it fairly clear how we need to handle the task of getting an egg, before we've started actually writing any code

Collecting data

Spoiler

Most scripts are going to need a considerable amount of data gathered, this data is generally stored as a static constant. 

1. I have created a data package under my scripts folder

image.png.d77026053bd3f0871a6fee51725788ac.png

2. I have created a Constants class in the data package

image.png.c0a2bcffd759bf409a68f32c31ed5041.png

package scripts.data;

import org.tribot.script.sdk.types.Area;
import org.tribot.script.sdk.types.WorldTile;

public class Constants {
    public static final int EGG = 1944;
    public static final int BUCKET = 1925;
    public static final int BUCKET_OF_MILK = 1927;
    public static final int GRAIN = 1947;
    public static final int POT = 1931;
    public static final int POT_OF_FLOUR = 1933;

    public static final Area COW_PEN = Area.fromRadius(new WorldTile(3258, 3276, 0), 4);
    public static final Area CHICKEN_COOP = Area.fromRadius(new WorldTile(3230, 3298, 0), 2);
    public static final Area BUCKET_AREA = Area.fromRadius(new WorldTile(3228, 3291, 0), 2);
    public static final Area WHEAT_FARM = Area.fromRadius(new WorldTile(3158, 3299, 0), 4);
    public static final Area FLOUR_MILL_TOP = Area.fromRadius(new WorldTile(3166, 3307, 2), 2);
    public static final Area FLOUR_MILL_GROUND = Area.fromRadius(new WorldTile(3166, 3307, 0), 2);
    public static final WorldTile COOKS_KITCHEN = new WorldTile(3209, 3213, 0);

    public static final int FLOUR_BIN_SETTING = 695;

    public static final String[] COOK_DIALOGUE = {"What's wrong?", "Yes."};
}

Setting up an Enum to handle states

Spoiler

An enum should be used anytime we need to represent a fixed set of constants. An example in the context of this tutorial is an enum type that would have a field for an item id and a method to determine if the inventory contains the item id.

I have created an enum type called Ingredient in the data package of our script.

image.png.091b4b5557820bf4de730f80eb630b32.png

Using the Lombok annotation @AllArgsConstructor I don't need to worry about creating a constructor



package scripts.data;

import lombok.AllArgsConstructor;
import org.tribot.script.sdk.Inventory;

@AllArgsConstructor
public enum Ingredient {
    EGG(Constants.EGG),
    BUCKET_OF_MILK(Constants.BUCKET_OF_MILK),
    POT_OF_FLOUR(Constants.POT_OF_FLOUR),
    ;
    private int itemID;

    public boolean hasIngredient() {
        return Inventory.contains(this.itemID);
    }
}

Storing Variables

Spoiler

There are a few ways to go about storing variables that are used by your script.

One of the more common ones is to create a Vars object that is initialised at the start of the script and use that to get and set the values as your script runs.

Here I have created a Vars class under the data package.

image.png.3623628db2bfe029f94604d1b92f1a22.png

I've populated it with commonly used variables, as well as added a CurrentIngredient object which will be used to store the current ingredient being gathered.



package scripts.data;

import lombok.Getter;
import scripts.Ingredient;

@Getter
public class Vars {

    private boolean isRunning = true;

    private String status = null;
    
    private Ingredient currentIngredient = null;

    private static Vars instance = new Vars();

    public static Vars get() {
        return instance;
    }

}

Creating the Gathering Tasks

Spoiler

First I am going to create a package named tasks that sits under my scripts package:

image.png.1936896c358981b41e3543de4ea5a47d.png

Then I'll make a template task



package scripts.tasks;

import scripts.Priority;
import scripts.Task;

public class TemplateTask implements Task {
    @Override
    public Priority priority() {
        return Priority.MEDIUM;
    }

    @Override
    public boolean validate() {
        return false;
    }

    @Override
    public void execute() {

    }
}

Next I will copy and paste it 3 times (once for each item to gather):

image.png.dd04d3448121277aa75ec4b06d67c6fb.png

OK, let's start with the egg task



package scripts.tasks;

import org.tribot.script.sdk.Log;
import org.tribot.script.sdk.Waiting;
import org.tribot.script.sdk.query.Query;
import org.tribot.script.sdk.util.TribotRandom;
import org.tribot.script.sdk.walking.GlobalWalking;
import scripts.Priority;
import scripts.Task;
import scripts.data.Constants;
import scripts.data.Ingredient;
import scripts.data.Vars;

public class EggTask implements Task {
    @Override
    public Priority priority() {
        return Priority.MEDIUM;
    }

    @Override
    public boolean validate() {
        // We only want to execute this task when the current ingredient is equal to egg AND we do not have an egg in our inventory
        return Vars.get().getCurrentIngredient().equals(Ingredient.EGG) && !Vars.get().getCurrentIngredient().hasIngredient();
    }

    @Override
    public void execute() {
        if (!Constants.CHICKEN_COOP.containsMyPlayer()) {
            Log.info("We need to go to the chicken coop, to get an egg");
            if (GlobalWalking.walkTo(Constants.CHICKEN_COOP.getRandomTile()) && Waiting.waitUntil(Constants.CHICKEN_COOP::containsMyPlayer)) {
                //This code will execute if we have successfully clicked on a random tile in the chicken coop
                // and we have successfully waited until we are in the chicken coop
                Waiting.waitNormal(600, 90); // let's sleep before the next action
            } else {
                //this code will execute if we have either failed to walk to the random tile in the chicken coop,
                // or the waiting condition has timed out before we make it to the coop
                return; //lets return early as we don't want to execute anything underneath this
            }
        }
        if (takeEgg() && Waiting.waitUntil(TribotRandom.uniform(1800, 2400), () -> Vars.get().getCurrentIngredient().hasIngredient())) { 
            //this code will execute if the take egg method returns true AND waiting until the player has the curent ingredient succeeds
            //note that I've added a uniform timeout to the Waiting.waitUntil method here
            Waiting.waitNormal(600, 90);
        }
    }

    private boolean takeEgg() {
        return Query.groundItems() //An egg is a ground item, so let's query those
                .idEquals(Constants.EGG) //We only want ground items that have an id matching an eggs
                .findBestInteractable() // Let's make tribot decide which egg to get
                .map(egg -> egg.interact("Take")) //if there is an egg let's try to pick it up
                .orElse(false); // if there is not an egg, or taking it fails, let's return false
    }
}

Now let's move onto the milk task

package scripts.tasks;

import org.tribot.script.sdk.Inventory;
import org.tribot.script.sdk.Waiting;
import org.tribot.script.sdk.query.Query;
import org.tribot.script.sdk.util.TribotRandom;
import org.tribot.script.sdk.walking.GlobalWalking;
import scripts.Priority;
import scripts.Task;
import scripts.data.Constants;
import scripts.data.Ingredient;
import scripts.data.Vars;

public class MilkTask implements Task {
    @Override
    public Priority priority() {
        return Priority.MEDIUM;
    }

    @Override
    public boolean validate() {
        // We only want to execute this task when the current ingredient is equal to bucket of milk AND we do not have a bucket of milk in our inventory
        return Vars.get().getCurrentIngredient().equals(Ingredient.BUCKET_OF_MILK) && !Vars.get().getCurrentIngredient().hasIngredient();
    }

    @Override
    public void execute() {
        if (!Inventory.contains(Constants.BUCKET)) { //This code will only execute if we do not have a bucket - we'll need one before we can milk the cow!
            if (!Constants.BUCKET_AREA.containsMyPlayer()) {
                Vars.get().setStatus("Walking to bucket");
                if (GlobalWalking.walkTo(Constants.BUCKET_AREA.getRandomTile()) && Waiting.waitUntil(Constants.BUCKET_AREA::containsMyPlayer)) {
                    Waiting.waitNormal(600, 90);
                }
            }
            if (Constants.BUCKET_AREA.containsMyPlayer()) {
                Vars.get().setStatus("Taking Bucket");
                if (takeBucket() && Waiting.waitUntil(TribotRandom.uniform(1800, 2400), () -> Inventory.contains(Constants.BUCKET))) {
                    //this code will execute if the take bucket method returns true AND waiting until the inventory contains an item that matches the bucket item id
                    //note that I've added a uniform timeout to the Waiting.waitUntil method here
                    Waiting.waitNormal(600, 90);
                }
            }
            //lets return here, because assuming all the code has executed successfully the inventory will now contain a bucket and this code will not execute again
            return;
        }
        if (dairyCowDistance() > TribotRandom.normal(4, 6, 5, 1)) {
            // lets only trigger this code if a dairy cow is not nearby
            Vars.get().setStatus("Walking to cow pen");
            if (GlobalWalking.walkTo(Constants.COW_PEN.getRandomTile()) && Waiting.waitUntil(Constants.COW_PEN::containsMyPlayer)) {
                Waiting.waitNormal(600, 90);
            }
        }
        if (Constants.COW_PEN.containsMyPlayer()) {
            Vars.get().setStatus("Milking Cow");
            if (milkCow() && Waiting.waitUntil(TribotRandom.uniform(2400, 4800), () -> Vars.get().getCurrentIngredient().hasIngredient())) {
                //this code will execute if the milkCow method returns true AND waiting until the player has the curent ingredient succeeds
                //note that I've added a uniform timeout to the Waiting.waitUntil method here
                Waiting.waitNormal(600, 90);
            }
        }
    }

    private boolean takeBucket() {
        return Query.groundItems() //Like the egg a bucket is a ground item, so let's query those
                .idEquals(Constants.BUCKET) //We only want ground items that have an id matching an a bucket
                .findBestInteractable() // Let's make tribot decide which bucket to get
                .map(bucket -> bucket.interact("Take")) //if there is a bucket let's try to pick it up
                .orElse(false); // if there is not a bucket, or interacting with it fails, let's return false
    }

    private boolean milkCow() {
        return Query.gameObjects() //dairy cows are game objects so that's what we'll query
                .nameEquals("Dairy cow") //We only want game objects that have a name matching Dairy cow
                .findBestInteractable() // Let's make tribot decide which dairy cow to get to get
                .map(cow -> cow.interact("Milk")) //if there is a dairy cow let's try to milk it
                .orElse(false); // if there is not a cow, or interacting with it fails, let's return false
    }

    private int dairyCowDistance() {
        return Query.gameObjects() //dairy cows are game objects so that's what we'll query
                .nameEquals("Dairy cow") //We only want game objects that have a name matching Dairy cow
                .findClosest() // we only care about the closest in this case
                .map(cow -> cow.distance()) // let's grab the distance of the closest cow
                .orElse(100); // if there is no nearby cow, let's assume it is 100 tiles away
    }
}

Now let's move onto the flour task, usually it would be better to separate this code even further, with a PickWheatTask/MakeGrainTask... in order to ensure the code is concise and readable



package scripts.tasks;

import org.tribot.script.sdk.GameState;
import org.tribot.script.sdk.Inventory;
import org.tribot.script.sdk.Waiting;
import org.tribot.script.sdk.query.Query;
import org.tribot.script.sdk.util.TribotRandom;
import org.tribot.script.sdk.walking.GlobalWalking;
import scripts.Priority;
import scripts.Task;
import scripts.data.Constants;
import scripts.data.Ingredient;
import scripts.data.Vars;

public class FlourTask implements Task {
    @Override
    public Priority priority() {
        return Priority.MEDIUM;
    }

    @Override
    public boolean validate() {
        // We only want to execute this task when the current ingredient is equal to pot of flour AND we do not have an egg in our inventory
        return Vars.get().getCurrentIngredient().equals(Ingredient.POT_OF_FLOUR) && !Vars.get().getCurrentIngredient().hasIngredient();
    }

    @Override
    public void execute() {
        if (!Inventory.contains(Constants.POT)) { //This code will only execute if we do not have a bucket - we'll need one before we can milk the cow!
            int distance = TribotRandom.normal(3, 6, 4, 1);
            if (Constants.COOKS_KITCHEN.distance() > distance) {
                Vars.get().setStatus("Walking to Pot");
                if (GlobalWalking.walkTo(Constants.COOKS_KITCHEN) && Waiting.waitUntil(() -> Constants.COOKS_KITCHEN.distance() <= distance)) {
                    Waiting.waitNormal(600, 90);
                }
            }
            if (Constants.COOKS_KITCHEN.distance() <= distance) {
                Vars.get().setStatus("Taking Pot");
                if (takePot() && Waiting.waitUntil(TribotRandom.uniform(1800, 2400), () -> Inventory.contains(Constants.POT))) {
                    //this code will execute if the takePot method returns true AND waiting until the inventory contains an item that matches the pot item id
                    //note that I've added a uniform timeout to the Waiting.waitUntil method here
                    Waiting.waitNormal(600, 90);
                }
            }
            //lets return here, because assuming all the code has executed successfully the inventory will now contain a pot and this code will not execute again
            return;
        }
        if (isFlourBinEmpty()) {
            if (!Inventory.contains(Constants.GRAIN)) {
                if (!Constants.WHEAT_FARM.containsMyPlayer() && GlobalWalking.walkTo(Constants.WHEAT_FARM.getRandomTile()) && Waiting.waitUntil(Constants.WHEAT_FARM::containsMyPlayer)) {
                    //This code will execute if we are not initially in the wheat from
                    // then have successfully clicked on a random tile in the wheat farm
                    // and we have successfully waited until we are in the wheat farm
                    Waiting.waitNormal(600, 90); // let's sleep before the next action
                }
                if (Constants.WHEAT_FARM.containsMyPlayer()) {
                    Vars.get().setStatus("Picking Wheat");
                    if (pickWheat() && Waiting.waitUntil(TribotRandom.uniform(1800, 2400), () -> Inventory.contains(Constants.GRAIN))) {
                        //this code will execute if the pickWheat method returns true AND waiting until the inventory contains an item that matches the grain item id
                        //note that I've added a uniform timeout to the Waiting.waitUntil method here
                        Waiting.waitNormal(600, 90);
                    }
                }
                //lets return here, because assuming all the code has executed successfully the inventory will now contain grain and this code will not execute again
                return;
            }
            if (!Constants.FLOUR_MILL_TOP.containsMyPlayer()) {
                Vars.get().setStatus("Walking to the Top of the Flour Mill");
                if (GlobalWalking.walkTo(Constants.FLOUR_MILL_TOP.getRandomTile()) && Waiting.waitUntil(Constants.FLOUR_MILL_TOP::containsMyPlayer)) {
                    Waiting.waitNormal(600, 90); // let's sleep before the next action
                }
            }
            if (Constants.FLOUR_MILL_TOP.containsMyPlayer()) {
                if (fillHopper() && Waiting.waitUntil(() -> !Inventory.contains(Constants.GRAIN) && !MyPlayer.isAnimating())) {
                    Waiting.waitNormal(600, 90);// let's sleep before the next action
                }
                if (operateControls() && Waiting.waitUntil(() -> !isFlourBinEmpty())) {
                    // after operating with the hopper, the setting that stores the value for whether or not there is flour in the bin changes from 0 to 1
                    Waiting.waitNormal(600, 90);// let's sleep before the next action
                }
            }
        }
        if(!isFlourBinEmpty()){
            if (!Constants.FLOUR_MILL_GROUND.containsMyPlayer()) {
                Vars.get().setStatus("Walking to the Ground of the Flour Mill");
                if (GlobalWalking.walkTo(Constants.FLOUR_MILL_GROUND.getRandomTile()) && Waiting.waitUntil(Constants.FLOUR_MILL_GROUND::containsMyPlayer)) {
                    Waiting.waitNormal(600, 90); // let's sleep before the next action
                }
            }
            Vars.get().setStatus("Emptying Bin");
            if (emptyBin() && Waiting.waitUntil(TribotRandom.uniform(1800, 2400), () -> Vars.get().getCurrentIngredient().hasIngredient())) {
                //this code will execute if the emptyBin method returns true AND waiting until the player has the curent ingredient succeeds
                //note that I've added a uniform timeout to the Waiting.waitUntil method here
                Waiting.waitNormal(600, 90);
            }
        }
    }

    private boolean emptyBin() {
        return Query.gameObjects() // The flour bin is a game object, so let's query those
                .nameEquals("Flour bin") // We only want game objects named flour bin
                .findBestInteractable() // Let's make tribot decide which flour bin to get
                .map(wheat -> wheat.interact("Empty")) //if there is a flour bin let's try to empty it
                .orElse(false); // if there is no flour bin, or interacting with it fails, let's return false
    }

    private boolean operateControls() {
        return Query.gameObjects() // The hopper controls are a game object, so let's query those
                .nameEquals("Hopper controls") // We only want game objects named hopper controls
                .findBestInteractable() // Let's make tribot decide which hopper control to get
                .map(wheat -> wheat.interact("Operate")) //if there is a hopper controls object let's try to operate it
                .orElse(false); // if there is no hopper controls, or interacting with it fails, let's return false
    }


    private boolean fillHopper() {
        return Query.gameObjects() // The hopper is a game object, so let's query those
                .nameEquals("Hopper") // We only want game objects named hopper
                .findBestInteractable() // Let's make tribot decide which hopper to get
                .map(wheat -> wheat.interact("Fill")) //if there is a hopper object let's try to fill it
                .orElse(false); // if there is no hopper, or interacting with it fails, let's return false
    }

    private boolean pickWheat() {
        return Query.gameObjects() // Wheat is a game object, so let's query those
                .nameEquals("Wheat") // We only want game objects named wheat
                .findBestInteractable() // Let's make tribot decide which Wheat to get
                .map(wheat -> wheat.interact("Pick")) //if there is a wheat object let's try to pick it
                .orElse(false); // if there is no wheat, or interacting with it fails, let's return false
    }

    private boolean takePot() {
        return Query.groundItems() //the pot is a ground item, so let's query those
                .idEquals(Constants.POT) //We only want ground items that have an id matching a pot
                .findBestInteractable() // Let's make tribot decide which bucket to get
                .map(bucket -> bucket.interact("Take")) //if there is a bucket let's try to pick it up
                .orElse(false); // if there is not a bucket, or interacting with it fails, let's return false
    }

    private boolean isFlourBinEmpty() {
        // here we check if the flour bin has flour in it by checking the setting that stores the value, this value is gathered via the settings explorer
        return GameState.getSetting(Constants.FLOUR_BIN_SETTING) == 0;
    }
}

Goodness that's a lot of repeated code! Let's make it a bit tidier by making one method to handle all of our game object interactions! 






private boolean interactObject(String name, String action) {
    return Query.gameObjects() 
            .nameEquals(name) 
            .findBestInteractable()
            .map(wheat -> wheat.interact(action)) 
            .orElse(false); 
}

Great so now we have the code to gather flour, eggs and milk!

Turning in the Quest

Spoiler

Let's copy the TemplateTask class and paste it in the tasks package. I'll be calling mine "TurnInQuestTask"

image.png.483a192ef81de74abacb7dff759b05f2.png



package scripts.tasks;

import org.tribot.script.sdk.ChatScreen;
import org.tribot.script.sdk.Log;
import org.tribot.script.sdk.Waiting;
import org.tribot.script.sdk.query.Query;
import org.tribot.script.sdk.util.TribotRandom;
import org.tribot.script.sdk.walking.GlobalWalking;
import scripts.Priority;
import scripts.Task;
import scripts.data.Constants;
import scripts.data.Ingredient;
import scripts.data.Vars;

import java.util.Arrays;

public class TurnInQuestTask implements Task {
    @Override
    public Priority priority() {
        return Priority.MEDIUM;
    }

    @Override
    public boolean validate() {
        //we only want to execute this code when we have all the ingredients
        return Arrays.stream(Ingredient.values()).allMatch(Ingredient::hasIngredient);
    }

    @Override
    public void execute() {
        int distance = TribotRandom.normal(4, 1);
        if (Constants.COOKS_KITCHEN.distance() > distance) {
            Vars.get().setStatus("Walking to Cook");
            if (GlobalWalking.walkTo(Constants.COOKS_KITCHEN) && Waiting.waitUntil(() -> Constants.COOKS_KITCHEN.distance() <= distance)) {
                Waiting.waitNormal(600, 90);
            }
        }
        if(Constants.COOKS_KITCHEN.distance() <= distance){
            Vars.get().setStatus("Talking to Cook");
            if(talkToCook() && Waiting.waitUntil(()-> ChatScreen.isOpen())){
                Waiting.waitNormal(600, 90);
                if(ChatScreen.handle(Constants.COOK_DIALOGUE) && Waiting.waitUntil(()->Arrays.stream(Ingredient.values()).noneMatch(Ingredient::hasIngredient))){
                    Log.info("Successfully handed in quest");
                    //YAY! This code will only execute if we successfully handle the Cook's chatscreen AND our ingredients are taken from our inventory
                }
            }
        }
    }

    private boolean talkToCook(){
        return Query.npcs()//The cook is an npc, so let's query those
                .nameEquals("Cook") //We only want npcs named cook
                .findBestInteractable() // Let's make tribot decide which egg to get
                .map(cook -> cook.interact("Talk-to")) //if there is a cook let's try to Talk-to him
                .orElse(false); // if there is not a cook, or Talking to him fails, let's return false
    }
}

 

 Putting it together

Spoiler

First let's create a taskset in Vars which contains each of the tasks we've made!



private TaskSet tasks = new TaskSet(new EggTask(), new FlourTask(), new MilkTask(), new TurnInQuestTask());

Let's make some changes to our MyScript Class - starting with a rename (Refactor > Rename)

image.png.3386915879b8ff45b63283530e786b00.png

Let's update the script manifest with some meaningful information



@TribotScriptManifest(name = "A Cooks Tutorial", author = "SkrrtNick", category = "Quest", description = "Completes Cooks Assistant")

Let's remove the code from the script template in the execute method



@Override
public void execute(final String args) {

}

Now let's add a while loop that iterates over each of the tasks 



     while (Vars.get().isRunning()) {

         for (Task task : Vars.get().getTasks()) {
             if (task.validate()) {
                 task.execute();
             }
         }

         Waiting.waitUniform(20,40); // we need this sleep here to prevent iterating too quickly
     }

Great! 

Now we need a way to reassign an ingredient if we have it (or if the value is null e.g. at the start of the script)



    while (Vars.get().isRunning()) {

        if (Vars.get().getCurrentIngredient() == null || Vars.get().getCurrentIngredient().hasIngredient()) {
            //we only want this code to execute if we do not have a currentIngredient set, or if we already ahve the currentIngredient
            Arrays.stream(Ingredient.values()) // this converts the array of Ingredients into a Stream so that we can operate on it
                    .filter(ingredient -> !ingredient.hasIngredient()) // lets only choose from ingredients that we don't have
                    .findAny() // we can choose any as the order in which we grab ingredients does not matter
                    .ifPresent(nextIngredient -> Vars.get().setCurrentIngredient(nextIngredient)); // lets assign the result, if any to currentIngredient
        }

        for (Task task : Vars.get().getTasks()) {
            if (task.validate()) {
                task.execute();
            }
        }

        Waiting.waitUniform(20, 40); // we need this sleep here to prevent iterating too quickly
    }
}

Let's make sure the login bot and break handler are enabled by overriding the configure method and setting their values as true.

@Override
public void configure(@NotNull ScriptConfig config) {
    config.setRandomsAndLoginHandlerEnabled(true);
    config.setBreakHandlerEnabled(true);
}

Adding a Paint

Spoiler

Making a pretty paint has never been easier.

In the execute method of our main class (but not in the while loop) we can add this code:



PaintTextRow runningPaintTemplate = 
        PaintTextRow.builder()
        .background(new Color(120, 123, 128, 180)) // this is the bg colour for our rows
        .build();
BasicPaintTemplate paint = BasicPaintTemplate.builder()
        .row(PaintRows.scriptName(runningPaintTemplate.toBuilder())) //this method will use the manifest to grab the script name
        .row(PaintRows.runtime(runningPaintTemplate.toBuilder())) //this method will automatically print the value of runtime
        .row(runningPaintTemplate.toBuilder().label("Status")
                .value(() -> Vars.get().getStatus()).build()) // we can also display our own values 
        .row(runningPaintTemplate.toBuilder().label("Current Ingredient")
                .condition(()->Vars.get().getCurrentIngredient() != null) // we can assign conditions which will dictate whether or not a value is displayed
                .value(() -> Vars.get().getCurrentIngredient()).build()) 
        .location(PaintLocation.BOTTOM_LEFT_VIEWPORT)
        .build();
Painting.addPaint(i -> paint.render(i)); // this is the most important part of painting, actually doing the render

 

Video of the final product

 

You can also get my source here: https://github.com/SkrrtNick/a-cooks-tutorial

Edited by SkrrtNick
update links and embeds for new forums

image.png

Link to comment
Share on other sites

Wow! Looks really good looks like you put a lot of hard work into this one of the best tutorials i have seen to get you going and straight into it.

Edited by ELON

ELON SCRIPTS

 394be3a8-5eaf-4266-b234-01e95300be59.png.76cba13888011134cd5915f53a665c21.png 621afd0f-83ca-4b71-a843-9150a9da770c.png.fcb652c7ae868c992f1c0db3eebff152.png 49d2ac52-402b-48ee-985c-c843bd89e5f8.png.a8022edbba18146a72dbb32e9e5e3071.png 0977182c-9141-4343-8620-961534956fa7.png.39f8e80ec2ee2e36d8152fcf1790f6b0.png ttt.png.24a94083f264620b7a9d8407b70dfae0.png

SUPPORT

 discordImage.png

Link to comment
Share on other sites

  • 1 month later...

Yoo bro real nice tutorial, had a hard time finding any explaination. But i still have a little trouble with the code:

I need to say I just started scripting for OSRS but starting to get better at it and I have done some random map scripting and other types of coding.

public static Vars get() {
    return instance;
}

this code is in de Vars class but is called up on in the flour and milk task class

if (Constants.COOKS_KITCHEN.distance() > distance) {
    Vars.get().setStatus("Walking to Pot");
    if (GlobalWalking.walkTo(Constants.COOKS_KITCHEN) && Waiting.waitUntil(() -> Constants.COOKS_KITCHEN.distance() <= distance)) {
        Waiting.waitNormal(600, 90);
    }

the Vars.get().setStatus("Walking to Pot"); gives the error because it says it does not know how to resolve method setStatus in Vars.

I dont see setStatus in the code i only find the public string Status

private String status = null;

followed by the code 

private static Vars instance = new Vars();

public static Vars get() {
    return instance;
}

Can anyone hit me in the right direction, I get the point something is missing but dont know what.

Thanks in advance !!

Edited by dizz.nationl
Link to comment
Share on other sites

  • 3 months later...

I’m not sure if I’m the only one struggling slightly but then again I’m not sure many would even attempt with zero coding experience 😂 

 

A video would make this a 10/10

 

Currently experimenting with AI written code but I need to get to that stage first 🤣

Link to comment
Share on other sites

1 hour ago, trentnz said:

I’m not sure if I’m the only one struggling slightly but then again I’m not sure many would even attempt with zero coding experience 😂 

 

A video would make this a 10/10

 

Currently experimenting with AI written code but I need to get to that stage first 🤣

I would definitely suggest avoiding AI code if you want to learn - I assume you mean Chat GPT?

I would also suggest that you spend a little time learning Java before you start scripting!

cheers!

image.png

Link to comment
Share on other sites

  • 3 months later...
  • Nullable pinned this topic
  • 2 months 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...