The Complete Tutorial to Script Development


  • VIP

    About me
    I am Jeremy Wu, former RSPeer developer (ex-slacker too, Zach is the new slacker). I have been in the RuneScape scene for roughly 8-9 years. I have released some extremely popular scripts on other platforms in the past however, I won't disclose which ones to keep my real identity to just my irl name.

    Getting Started: Configuring the IDE
    Instead of reinventing the wheel, I will redirect you to yasper's very informative thread on configuring your IDE. It can be found at: Setting up intellij idea for rspeer script development

    API
    The most important tools for creating your script come from RSPeer's API. Without it we wouldn't be able to get data or interact with the game.

    For the most up to date information on the API you should go to the javadocs .

    The API naming is very literal and straight forward further into the guide I will elaborate on how to select the methods from the API that you will need.

    Commonly used API

    • Player - The game characters in RuneScape including your own (getLocal())
    • Npc - Non playing characters such as monsters (goblins, cows etc), non-attackable (slayer masters, quest figures etc)
    • SceneObject - Objects in the games like doors, chests, bank booths etc
    • Bank - Methods for interacting with the bank of RuneScape
    • Interfaces - Used for RuneScape dialogues and interfaces like chatbox and store interfaces
    • Movement - Used for moving your character
    • Skills - To get skill data like level, current level and experience

    The Script Skeleton

    With your IDE configured you can create a new java file for your script. To make your java class able to run as a script you will need to do a few things. First is you'll need to make it a subclass of Script and secondly you need add a ScriptMeta annotation for it be read.

    import org.rspeer.script.Script;
    import org.rspeer.script.ScriptMeta;
    import org.rspeer.script.ScriptCategory;
    
    @ScriptMeta(name = "Script Name",  desc = "Script description" developer = "Developer's Name", category = ScriptCategory.CATEGORY)
    public class FirstScript extends Script {
    
        @Override
        public int loop() {
            return 0;
        }
    }
    

    The ScriptMeta is self explanatory so I will jump into Script.

    Inside of Script.class is an abstract method called public int loop() since this is the only abstract method it will be the only one mandatory inside of your script. It is the most vital part as it is what repeats your task. There are two other useful methods inside the Script class and it's super class LoopTask, they respectively are public void onStart() and public void onStop(). The script skeleton with these methods will now become:

    import org.rspeer.script.Script;
    import org.rspeer.script.ScriptMeta;
    import org.rspeer.script.ScriptCategory;
    
    @ScriptMeta(name = "Script Name",  desc = "Script description", developer = "Developer's Name", category = ScriptCategory.CATEGORY)
    public class FirstScript extends Script {
    
        @Override
        public void onStart() {
            //When the script is first started the segment of code in this method will be ran once.
        }
    
        @Override
        public int loop() {
            //Code in here starting from the top-down will be ran and repeated.
            return 0; // The rate of repeat is defined by the returning int, this number represents milliseconds. 1000ms = 1 second.
        }
    
        @Override
        public void onStop() {
            //When the script is stopped the segment of code in this method will be ran once.   
        }
    }
    

    Starting your first script
    The first thing you need to create a script is the idea of what it's for. For this tutorial we will be creating a woodcutting script.

    The way I personally like to make a script is to start using pseudo code (a notation resembling a simplified programming language, used in program design.) to figure out what API and data is needed.

    In my mind I know that the woodcutting script will need to cut down trees, and dispose of logs (via dropping or banking).

    The next thing I start to think about are the conditions that are needed to do one of those tasks. For example, if I was filling up my car with gas I would only release the gas into my tank if the nozzle is in my car. If my nozzle was not in my car I would only put it in my car if my gas tank was open. If my gas tank wasn't open then I would open it.

    To simplify this for our script we have 3 tasks, cutting, banking and dropping.

    What conditions do we need if we are cutting?

    • We can only cut trees if our inventory has space for the logs
    • We should only cut trees if our player isn't already cutting a tree
    • We should only cut trees if our player isn't moving to a tree
    • We should only cut trees if we are in the appropriate area for cutting trees

    and the banking conditions?

    • If we are at the bank
    • If our inventory is full
    • If we want to bank

    and finally the dropping conditions?

    • If our inventory is full
    • If we want to drop the logs

    Now I write this in pseudo code like I said, I put this directly into the IDE with comments, so what it would look like is:
    pseudo code

    We have our conditions and our tasks laid out, but we should go over it one more time to make sure we aren't missing anything. When reviewing this I notice that there are two instances where the script could break. These are at the parts where we check to see if we are in the right area for the task. If we aren't then the script won't do anything. So I'll add an else statement telling the script to walk to the area.

    Finalised Pseudo code

    Alright now we are done with our pseudo code for the structure.

    The next part is to start filling out the statements and tasks with actual code using the API. Like I said before the API has very literal naming. In the most common API tools listed above or simply by browsing the javadocs you notice the Player class. This is what we will use for characters in the game that are played by real people. More specifically, we can use this for our own character as well.

    Using the javadocs, navigate to the class you want to use. In this case I went to Player and saw that it inherits two methods I can use. These are isMoving() and isAnimating().

    Player methods

    To use these I need an instance of our own player. To get instances of Player we can use the methods found in the class with the pluralised name of Player (Players). Players.getLocal() get's our character. I save this two a variable because I use it more than once and it's shorter to type local than Players.getLocal(), It also only grabs it from the client once saving resources.

    Starting the actual code

    Now if you're still new to java, a friendly reminder is that !local.isMoving() is equivalent to local.isMoving() == false. By putting ! infront of a boolean you receive the opposite value, so if it's expected to be true the ! makes it false.

    I'm going to go ahead and fill in everything we have code for and then we can move on with the actual tasks.

    Finished conditions

    A few things to note, I added a boolean for dropping logs, while this is a perfectly good way of doing it I use it here as a placeholder, I will show you how I will actually do it further into the tutorial.

    I added both the areas using Area.rectangular which makes a rectangular area of all the tiles from the first tile that's most northern and western to the tile that is most southern and eastern.

    These areas also do not change in value so I make them into a constant (static final) and java naming convention dictates that constants are capitalised and spaces between words are represented with an underscore.

    Lastly, for area.contains(Positionable p);, while it accepts a Position, it is not needed and more characters of code when you can simply pass a Positionable. That page has all of the subclasses of Positionable (Which Player is).

    Writing the tasks

    Now we can finally add the parts that interact with the game.

    Anything that can be interacted with whether is be a player, an object, an item etc has the method interact(). This means we only need an instance of what we're interacting with and to pass an action to it.

    We can start with the task that drops logs, we are going to get the item in a similar fashion to how we got our local player.

    Now items aren't just found in the inventory, there can be some in the bank or on the ground. So instead of there being an Items.class we using the class where we want the items to be from. In our case this will be the Inventory.class.

    The last thing to note before we actually write this, is that there are going to be more than one log in the inventory when dropping. So instead of saving the first log to a variable we are just going to use a loop for all the logs and drop them.

    Our method look's like:

    for (Item log : Inventory.getItems(item -> item.getName().equals(logName))) {
        log.interact(DROP_ACTION);
        Time.sleep(300);
    }
    

    DROP_ACTION is just a constant of the string "Drop" which is the text you would find in the menu of right clicking the item.

    I also added a Time.sleep(300); So the script doesn't drop insanely fast (or skip dropping some logs).

    Let's move on to the banking part of the script. Firstly, we want to check if the bank is opened, if it isn't we want to open it.

    If it is already open we want to deposit everything except the axe incase the player isn't wielding the axe. Once again, using the javadocs we find a Bank.class and it's methods which are literal in name.

    First I add the constant AXE_PREDICATE for all axes. Inside of this instead of checking the item name for all names of the axe, I use some common sense and see all types of axe contain the word axe at the end. So I just check if the name contains axe.

    private static final Predicate<Item> AXE_PREDICATE = item -> item.getName().contains("axe");

    Banking method

    I will come back and complete the open bank part further on in the tutorial. This is because as an all in one woodcutter some banks might be different.

    I am going to now do the woodcutting task and then finish up with the traversing tasks.

    Trees are a type of SceneObject so we will use that to get the instance of one and interact with it. In this case we will null check. We didn't null check the logs from the drop task because we load it in an array which if there are none then it just wouldn't loop through anything. We also didn't null check the local player because our character is always going to be there. The only instance of our local player not being there is if we are logged off. We will add a logged in check further on.

    Woodcutting method

    Once again the CUT_ACTION is just the action to cut, I don't remember what the actual action is so I will recheck it in the in-game data collecting segment.

    private static final String CUT_ACTION = "Chop down";
    

    treeName is also another temporary variable which I will change when I show you a better way of storing this information.

    I'm going to finish up by adding walking. At the time of writing this guide the web isn't out, however; expecting it's release I'll just add in the code Movement.walkTo(Positionable position). Now what most people would do is find a Position to add it in, and while this is acceptable there is a better way. I saw on the JAVADOCS that Area contains a method called TREE_AREA.getCenter() which returns a Position so I will just use that.

    In the areas where I commented //Walk to bank and //Walk to trees I added Movement.walkTo(BANK_AREA.getCenter()); and Movement.walkTo(TREE_AREA.getCenter()); respectively.

    The Task Framework

    This code once filled out with the proper data will work as intended, however; we can make it more manageable and more appealing with a task framework. Getting use to this framework is good because it makes developing more advanced scripts easier and again more manageable.

    To use the task framework you must go back to the line:

    public class WuCutting extends Script {
    

    and change Script to TaskScript so it becomes:

    public class WuCutting extends TaskScript {
    

    Next inside of my script's packaging I add a new package. You can call this node, task, impl etc. I just name it impl. This is where you store the separate tasks which will all be in a class of their own.

    Remember that we had 3 tasks from before and a fourth one when we went back to review. The Cut, Drop, Bank and then Walking tasks. We will create separate classes for each of these in the package.

    Tasks

    Now let's start transferring our tasks into these classes. For each task class you want to extend the class called Task and inherit it's methods.

    Task class

    Now the validate method is where you put the condition of when it should execute and the execute method is obviously what it should do when it's condition is met. REMEMBER: do not level the execute return value at 0, make it something like 300 (300 ms).

    Let's go through this now and see how to fill it out starting with traverse.

    First I changed the BANK_AREA and TREE_AREA to public to access them outside of the main class, but once again these two fields will be removed soon in place of a better storage method.

    I then need the conditions needed for traversing.

    Here's a visual guide to getting the conditions of a task from your loop.

    There are two sets of conditions needed, one for walking to the bank (Full inventory, not dropping, and not being at the bank) and one for walking to the trees (Inventory has space and isn't at the trees already)

    Now there are 5 booleans at play here, but the two conditions are different depending on how they're used. By placing multiple booleans in brackets it'll only return true of all of the booleans inside the bracket are true. We will put the 3 conditions needed for walking to the bank in a set of brackets, and then 2 booleans for walking to the trees in a set of brackets. The inside of the brackets will all use && (and) and the two pairs will be separated with || (OR) because the conditions in the brackets all need to be true for it to walk however only one set of those conditions (walking to the bank or walking to the trees) need to be true to walk, you can both need to walk to the bank and walk to the trees.

    Next I will add in the tasks we already made, now if we go through those conditions we see the only thing the script does is either Movement.walkTo(BANK) or Movement.walkTo(TREES), so all we need is the Movement.walkTo method in execute. For this I am going to move the conditions in the validate into a getter so I can reuse them.

    Our traverse class now looks like
    Traverse

    The reason why I add them to their own getter methods is so I don't have to retype all those conditions. The reason that I need to reuse them is so Movement knows where to walk. Some people might do

    if (traverseToBank()) {
        Movement.walkTo(BANK_AREA.getCenter()); 
    } else {
        Movement.walkTo(TREE_AREA.getCenter()); 
    }
    

    But there is a better way of writing this believe it or not. Methods can accept conditions inside of them too.

    For example

    method(boolean ? true : false)
    

    Setting up the inside of a method with a boolean then a question mark you can then put what it will pass in the arguments if true followed by semi-colons and then what to pass if the boolean is false.

    So instead of traversing with that previous code it will now become Movement.walkTo(traverseToBank() ? WuCutting.BANK_AREA.getCenter() : WuCutting.TREE_AREA.getCenter());

    The traverse class is complete and now looks like:

    Completed Traverse

    I will go ahead and fill out the rest of the tasks.

    Woodcut Task
    Woodcut

    Drop Task
    Drop

    I just remembered I named the task to bank, Bank, but this conflicts with the API class so I renamed to Banking.

    Bank Task
    Banking

    Once again we will finish bank opening very soon.

    The last part of configuring the script to the Task Framework is to reopen the main class and delete the int loop() method and everything we put it in, remove any variables that we moved to their specific class, remove unused imports and then in the onStart method submit our tasks with the method inherited from TaskScript called public final void submit(Task... tasks)

    I personally just add all the tasks into a task array constant then pass that to the submit method. Which would make the main class now look like:

    Main class

    Now we're almost done our script, all we have left to do is get the required data, put the data in better storage, use some listeners and test it out.

    Storing our data
    Instead of having these variables here, I personally would use enums. I can add all trees to one enum, and all locations to another. Then I can create a GUI for the user to select which location and tree to use.

    I create a data package for the enums which now looks like:
    New Packaging

    Now thinking about it logically, I know that some locations don't have a certain tree. So I will use the Tree enum inside of the Location enum. This means I am going to start with the tree location.

    Now which data would I want for the trees?

    Well I remember in my temporary variables I have logName and treeName, so I would want those. In this tutorial I won't be making it progressive and changing types of trees based on levels, but in the future when walking is released I will add it so I am also going to add a field for the required level.

    I won't go into detail on how to use enums as you can find a in depth guide from oracle here: Enum Types.

    Without the information, my enum for Tree looks like

    Moving onto location, I know we want the area of the location for both where to cut trees, and where to bank. I also want to know which trees are in this Area.

    This would make the enum for location look like:

    Location Enum

    Before we get the data let's configure our script to use this enums.

    Let's go back to the main class and delete all those temporary values. Now let's add two variables location and tree. Remember to delete unused imports and the class will now look like:
    Main Class

    I didn't set the two new variables yet because that will be done when we add a GUI. We have to go through our tasks to change the missing variables now.

    For example in Woodcut

    public class Woodcut extends Task {
    
        private static final String CUT_ACTION = "Chop down";
    
        @Override
        public boolean validate() {
            return !Inventory.isFull() && WuCutting.TREE_AREA.contains(Players.getLocal());
        }
    
        @Override
        public int execute() {
            final SceneObject tree = SceneObjects.getNearest(WuCutting.treeName);
            if (tree != null) {
                tree.interact(CUT_ACTION);
            }
            return 300;
        }
    }
    

    is now

    public class Woodcut extends Task {
    
        private static final String CUT_ACTION = "Chop down";
    
        @Override
        public boolean validate() {
            return !Inventory.isFull() && WuCutting.location.getTreeArea().contains(Players.getLocal());
        }
    
        @Override
        public int execute() {
            final SceneObject tree = SceneObjects.getNearest(WuCutting.tree.getName());
            if (tree != null) {
                tree.interact(CUT_ACTION);
            }
            return 300;
        }
    }
    

    Listeners
    Before we continue with collecting the data I am going to go over listeners. This is because the way I like to gather data is just to use a RenderListener, there are other ways you can do this like setting up a generic script with a GUI like an interface explorer but for beginners this is a little easier. I might create a tool to explore nearby interactables and such so keep an eye out for that.

    The first thing to listeners is that there are a lot of them that you can use. For a list you can go to the javadoc's page on listeners.

    Like I said there are a lot of them, they all work in the same way so I will touch upon the RenderListener now for adding a paint to your script. I will briefly go over Varps and the listener for that at the end.

    All of these listeners are interfaces, which means you can implement them. The way of doing this is very literal to what I just said. The code on the line of your class name public class WuCutting extends TaskScript {. You add it after WuCutting if there is no extends ..., but because there is one (extends TaskScript) you add it after that.

    RenderListener is what we are working with right now, so it becomes public class WuCutting extends TaskScript implements RenderListener {

    After you will need to import the inherited methods. For this case it's:

     @Override
    public void notify(RenderEvent renderEvent) {
    
    }
    

    If you are using intellij you can do renderEvent and type a "." after for a list of methods. I notice a method called getSource() which returns a Graphic. This is what we will use to create graphics.

    The method (With commonly used methods) should look like:
    Common Methods

    The client built-in debugger already shows your player's location and in I will also be making a more advanced debugger which will be released in a new client patch soon but for the mean time, everything else we will use this listener to get.

    Since debugger has our player's location we don't need to add anything for location.

    For trees we also don't need anything as we can visually identify it through the game.

    This would be useful for say interfaces or the fields which aren't known through visually looking at the game (Player.getAnimationID() for instance). For this example, I will just use the RenderListener to find my animation and then identify everything we actually need for the script through the game itself.

    Setting up the animation id in the paint makes our script look like:

    @ScriptMeta(name = "WuCutting Name",  desc = "Automates the woodcutting skill.", developer = "Jeremy", category = ScriptCategory.WOODCUTTING)
    public class WuCutting extends TaskScript implements RenderListener {
    
        private static final Task[] TASKS = { new Banking(), new Drop(), new Traverse(), new Woodcut() };
    
        public static Location location;
        public static Tree tree;
    
        @Override
        public void onStart() {
           // submit(TASKS); Comment this out for now so the script doesn't run while gathering data.
        }
    
        @Override
        public void onStop() {
    
        }
    
        @Override
        public void notify(RenderEvent renderEvent) {
            Graphics g = renderEvent.getSource();
            g.drawString("My animation: " + Players.getLocal().getAnimation(), 30, 30); //x and y are destinations the size of a pixel on the canvas, canvas is 503x765
        }
    }
    

    Remember to just comment out the script so it doesn't run while gathering data, alternatively you could just pause the script.

    Let's compile this and run it!

    Move the compiled scripts to RSPeer>Scripts
    Script Location

    Now open the bot and run your script. In the following GIF you can see when I'm idle and my character doesn't change animation it's -1, when I cast air strike and my character animates it changes to 711

    Animation

    For the data we need we can start with tree names and log names.

    Tree name

    We see the action (which we were unsure of earlier) is "Chop down". We also see that this one is just Tree and required level 1 woodcutting. Let's add this data in!

    After finding all the data for trees I fill out the enum to look like:

    Tree Data

    Now for our Location Data we need the two areas. Using Draynor I am going to start with the bank.

    We need to find two positions opposing each other.

    Turning Debugger on we see that the first position is 3092, 3246, 0
    First Position

    and the second is 3097, 3240, 0

    Second Spot

    Do this at the woodcutting area too!

    Draynor one

    This one isn't far enough west (Also I baited the wizard to killing that guy so if it's your bot I'm sorry)
    Baited

    The x variable gets lower the more west you walk so I'm just going to subtract 5 tiles from it to make it work.

    I also only want the Willow Tree to be used in Draynor so after the Areas I will add only the Willow Tree. While I am in this enum I will add the Powercutting location with all the trees too. In the powercut for trees I will do Tree.value(); to grab all the Trees from the enum because you can powercut all of them.

    Location

    Just do this for all locations!

    Once that's all done we just need to fix the powercut variable and we are ready for the GUI.

    Banking
    Dropping

    GUI

    Ok the big bad scary GUI.

    First create a new class and extend JFrame. (Keeps your project organized and also doesn't require you to use a JFrame variable).

    In the constructor add the following methods
    Constructor

    Now we need to think about the controls we need. In the woodcutting script case we need to know which location and what tree to cut. We also need a button to start the script.

    We can use a combo box for both Location and Tree, we should also change the combo box with the trees depending on the location. We know the first Location in our enum is Powercut, so we can start the GUI with all the Trees in the tree box.

    Use JComboBox for the combobox, create a variable with it for the trees and then add Tree.values() inside it's arguments. Use the JFrame method add(JComponent) to add it to the gui.

    We can test the GUI as we make it by adding the main method.

    GUI Code
    GUI

    • The GUI will automatically match the bot when ran as a script.

    We're going to change the layout to make this slightly better to look at. Since we only have three components we can just use the FlowLayout. For other layouts you can use: A Visual Guide to Layout Managers.

    We can also add the other combobox and the button to start the script.

    Our GUI should now look something like:
    Layout and Other

    GUI

    To change the tree selection based on location we can add an actionlistener. Now remember for the combobox we added Locations and not Strings. So by adding an action listener to locationComboBoxwe can get the value of Location by getting the selected item of the locationComboBox and casting it. Then we can set the treeComboBox's model to a new DefaultComboBoxModel with the new location's trees.

    This might sound confusing but with an image it will become easier.

    Action Listener

    GUI

    Now we just need to set the initiate button. We will use an actionlistener here too!

    We are going to set the static location and tree variable from the main class with the location and tree we got from the selected items of the combo boxes.
    Then we are going to set the visibility of the GUI to false.

    Initiate

    The last part of our script is to make it only do anything when the GUI is done being setup.

    The location and tree variables are null until the initiate button is pressed so we can just add to our tasks the condition that these fields are not null.

    (WuCutting.tree != null && WuCutting.location != null)
    

    So the Banking class with it added would look like:
    Banking Class

    Do this for all your tasks!

    • Edit: After you are finished creating your GUI you can remove the public static void main(String...) method as this was just for testing.
    • In your script's main class you can initialise your GUI (new GUIClassName()) and whether you save to a variable or not you, simply add setVisible(true)
    @Override
    public void onStart() {
       new WuCuttingGUI().setVisible(true);
    }
    

    NOW YOU ARE READY! GO USE YOUR NEW SCRIPT!!!!



  • Looking forward to it! Looks like a great outline.


  • VIP

    wtf i was gonna do this


  • Director

    damn dude get a dark ide background


  • VIP

    @maddev No thanks


  • VIP

    lol good job



  • Looking good. Iā€™m sure a lot of new users will love you for this. šŸ™‚



  • There was a nullpointer error in the traverse class. is it because walkto has been deprecated ?



  • Nice job



  • Is the full script available anywhere or do we just piece it together from ur guide?



  • Nice tut. Looking forward to creating some scripts



  • is animating and is moving

    I assume animating is cutting a tree, moving is walking?

    also "wuCutting.loation.equuals(Location.POWERCUT);" not sure what POWERCUT is all about location powercut?



  • @chris121 here is where he's declaring POWERCUT as a location:
    0_1533890312240_04ad4cc1-d533-4898-b239-235367678fd3-image.png


  • Script Writer

    @King @chris121 From the enum looks like that it can cut anywhere, and will cut any tree that is inside Tree enum, hope that helps



  • @qverkk said in The Complete Tutorial to Script Development:

    @King @chris121 From the enum looks like that it can cut anywhere, and will cut any tree that is inside Tree enum, hope that helps

    Thanks qverk you legend. and for the 99 range=] but what about isAnimating, isMoving? and king thanks


  • Script Writer

    @chris121 for those I think I've replied in the other thread, you might want to check that thread out



  • I can't get the GUI to show up for some reason. how do i call it? when i try to start the script nothing happens. any help is appreciated



  • @rawrstin Jframe.setVisible(true);


  • VIP

    Late reply, couldn't remember my password because mad changed it to something retarded, I'll be editing the main post but I have a few comments directed about the GUI. The GUI is initialised in the the main method in the class, this is because we were using it to test there. I forgot to include the snippet in the tutorial (which I will add), however; like the tutorial explained the public void onStart() is ran when the script first runs, so it is this method you want to initialise the GUI. I keep the GUI in it's own class because it's the best way of doing it in most if not all scenarios. This keeps it organized and easy to modify. Initialising the GUI is the same as initialising any class (simply add GUIClassName gui = new GUIClassName();) after that you call the method setVisible(true) to display it. (gui.setVisible(true))



  • Excellent guide. Looking forward to breaking down this tutorial into some basic scripts.

    Appreciate the time and effort you made writing this up.


 

64
Online

3.1k
Users

442
Topics

5.0k
Posts