Skip to content
This repository has been archived by the owner on Jun 3, 2024. It is now read-only.

Creating Your First OSRS Bot

Kelltom edited this page Feb 14, 2024 · 22 revisions

Prerequisites

This tutorial will walk you through the basics of creating your first bot. It is helpful to understand the basics of object-oriented programming (variables, for loops, while loops, functions and classes). Before beginning, consider reading the Design document to better understand the architecture of OSBC. Please see the Developer Setup page for instructions on how to set up your development environment.

It is also important that your operating system is not running a scaled resolution. In other words, you must be running 100% scaling.

Warning

Botting/macroing in most online games is against the rules. Do so at your own risk. The development process of writing a bot is heavily based on trial and error, and testing can lead to your accounts looking suspicious and/or triggering automatic bot-detection methods employed by the game developers. I do not recommend using scripts on live, official game servers.


Part 1: The Basics

This is an interactive tutorial. It can be followed using a fresh account straight out of Lumby. If you intend to use OSBC for RSPS botting, the concepts of this tutorial will apply.

Create the Bot File

  1. In the src/model/osrs/ folder, duplicate the template.py file and rename it to something meaningful. For example, my_bot.py.
  2. Open this file in your editor and change the class name to something meaningful. It is best practice to prefix the class name with the game abbreviation. For example, class OSRSMyBot(OSRSBot).

Connect the Bot to the UI

  1. To make sure your bot is recognized by the OSBC UI, you must add a reference to it in the src/model/osrs/__init__.py file. Add the following line to the file:
    1. from .my_bot import OSRSMyBot
  2. Run the src/OSBC.py file to display the UI. You should see your bot listed under the OSRS tab.
  3. Close the UI for now.

Understanding Bot Class Hierarchy

OSBC bots follow a hierarchical structure, with the Bot baseclass at the top. It defines the functionality that all bots have (E.g., play, stop, check HP, drop items, etc). RuneLiteBot is an extension of this class, providing functionality specific to bots operating on RL-based games (E.g., locating outlined objects/npcs). Furthermore, each game should have a base class that extends from either Bot or RuneLiteBot. Doing so will allow you to define game-specific functionality (E.g., use a custom teleport interface in an RSPS). Finally, each bot should extend from the appropriate game-specific base class.

You may be wondering why we need OSRSBot (the OSRS bot base class) since it is practically empty. It's important to note that all bots have a game_title attribute, which dictates what appears in the "Select a Game" dropdown menu on the UI. The OSRSBot class once sets this attribute so all inheriting bots don't have to. Plus, it gives you an opportunity to define game-specific functionality in the future, if necessary.

Within your bot, use the self. keyword to access any properties/features of parent classes. Please look at src/model/bot.py and src/model/runelite_bot.py to see what functionality is available.

Understanding the Bot Template

The template.py file contains a basic bot skeleton. Starting with this template and modifying it as needed is a good idea.

All bots have 4 mandatory functions:

# imported modules here

class OSRSTemplate(OSRSBot):
    def __init__(self):
        '''Decides how the bot will appear on the UI.'''
        pass

    def create_options(self):
        '''Decides what option widgets will appear on options menu.'''
        pass

    def save_options(self, options: dict):
        '''Decides what to do with the values from the option menu.'''
        pass

    def main_loop(self):
        '''This function runs when the bot is started.'''
        pass

Log Messages

OSBC has a built-in logging system that allows you to log messages to the UI. This is useful for debugging and letting the user know what the bot is doing. To log a message, use the self.log_msg() function. Add the following code in your main_loop() function under the # -- Perform bot actions here -- comment to log a message every second:

            # -- Perform bot actions here --
            self.log_msg("Hello World!")
            time.sleep(1) # pause for 1 second

Test the Bot

  1. Run the src/OSBC.py file to display the UI.
  2. In the navigation pane on the left, click the dropdown menu and select "OSRS".
  3. Click Launch OSRS to start RuneLite. Locate the RuneLite executable if it is your first time. This launches the game with custom settings that OSBC needs to function.
    1. NOTE: If you are running RuneLite v1.9.11.2 or greater, OSBC may ask you to locate the Profile Manager folder as well. This is typically located in C:\Users\<username>\.runelite\profiles2. You must select the profiles2 folder, not the .runelite folder.
  4. In the game, log in to your account.
  5. Back in OSBC, select your bot from the list on the left.
  6. Open the Options panel and set the bot to run for 1 minute.
  7. In the OSBC UI, click the Play button to start the bot. Hover over the button to see a key-bound shortcut.
    1. Watch the UI for the Hello World! message every second.
    2. Note the progress bar increasing over time.
  8. Click Stop to stop the bot.
  9. Close the UI. You can keep RuneLite running.

RuneLite Settings

OSBC depends on a special RuneLite plugin configuration to work properly. OSBC only uses plugins found on the official Plugin Hub. When you launch RuneLite via OSBC's interface, it will look for a settings file in the src/runelite_settings/ folder with a name that matches this format: <game_title>_settings.properties. For more on configuring a bot to use a custom RuneLite plugin configuration, see Launching a Bot with Custom Settings.


Part 2: Options Menu

In this section, we will add more options to our bot's options menu. OSBC has 4 built-in widgets that you can use to collect user input:

  • Slider
  • Text Edit
  • Checkbox
  • Dropdown

Examples:

    def create_options(self):
        self.options_builder.add_slider_option("running_time", "How long to run (minutes)?", 1, 180)
        self.options_builder.add_text_edit_option("text_edit_example", "Text Edit Example", "Placeholder text here")
        self.options_builder.add_checkbox_option("multi_select_example", "Multi-select Example", ["A", "B", "C"])
        self.options_builder.add_dropdown_option("menu_example", "Menu Example", ["A", "B", "C"])

Add a Dropdown Option

  1. Open the src/model/osrs/my_bot.py file.
  2. In the __init__() function, add a variable that will store the user selection from the dropdown menu. We will call it self.take_breaks. Give it a default value of True.
  3. In the create_options() function, add a dropdown menu option that allows the user to select whether or not the bot should take breaks. The first argument should match the variable name, take_breaks and have the following options: ["Yes", "No"].
    def __init__(self):
        bot_title = "My Bot"
        description = "This is my bot's description."
        super().__init__(bot_title=bot_title, description=description)
        # Set option variables below (initial value is only used during UI-less testing)
        self.running_time = 1
        self.take_breaks = True

    def create_options(self):
        self.options_builder.add_slider_option("running_time", "How long to run (minutes)?", 1, 500)
        self.options_builder.add_dropdown_option("take_breaks", "Take breaks?", ["Yes", "No"])
  1. Test the bot again like before. When prompted to launch RuneLite, you can instead click skip if RuneLite is already running. You should now see the dropdown menu option in the UI.

Save the User's Selection

If you clicked "Save" in the previous step and received an error saying Unknown option: take_breaks, it's because we haven't added any code to handle the user's selection. Let's fix that.

  1. In the save_options() function, add a line of code that sets the self.take_breaks variable to the value of the take_breaks option. You can do this by using the options dictionary that is passed to the function. The options dictionary has the following format: {"option_name": option_value}.
    def save_options(self, options: dict):
        for option in options:
            if option == "running_time":
                self.running_time = options[option]
            elif option == "take_breaks":  # <-- Add this line
                self.take_breaks = options[option] == "Yes" # <-- Add this line
            else:
                self.log_msg(f"Unknown option: {option}")
                print("Developer: ensure that the option keys are correct, and that options are being unpacked correctly.")
                self.options_set = False
                return
        self.log_msg(f"Running time: {self.running_time} minutes.")
        self.log_msg(f"Bot will{' ' if self.take_breaks else ' not '}take breaks.") # <-- Add this line
        self.log_msg("Options set successfully.")
        self.options_set = True

We know that the dropdown will either give us "Yes" or "No", so we can use a ternary operator to set the self.take_breaks variable to True or False depending on the user's selection. This just makes it easier to use later on.

  1. Test the bot again. You should see a message saying options were set successfully.

We will make use of this option in later sections.


Part 2.5: Headless Testing

Tired of stumbling through the UI every time you want to test your bot? You can run your bot in headless mode.

Visit the Testing a Bot Without the UI page for more information.


Part 3: Utilities

OSBC has built-in utilities that can be used to enhance bot functionality. Many OSBC utilities have already been bundled into easy-to-use functions that belong to Bot or RuneLiteBot (and thus, can be accessed using the self. keyword). Alternatively, you can import utilities directly into your bot file and use them as needed.

Window & Mouse

By default, when a bot is started, OSBC scans your monitor for the game window, then performs a series of image-matching operations to locate various UI elements. This process is handled automatically by the Window utility, and the results are stored in a Bot property called win. After this happens, the main_loop() begins.

206949051-16e1bf57-a189-4eda-bbb2-1864e2849c45

Let's add some code to our main_loop that moves the mouse around various UI regions. We can do this using the Mouse utility. All bots have a mouse property for doing so.

Since these utilities are innate to all bots, we can access them using the self. keyword.

  1. Modify your main_loop() function to look like this:
    def main_loop(self):
        # Setup APIs
        # api_m = MorgHTTPSocket()
        # api_s = StatusSocket()

        # Main loop
        start_time = time.time()
        end_time = self.running_time * 60
        while time.time() - start_time < end_time:

            # -- Perform bot actions here --
            self.mouse.move_to(self.win.game_view.get_center())
            time.sleep(1)
            self.mouse.move_to(self.win.minimap.get_center())
            time.sleep(1)
            self.mouse.move_to(self.win.control_panel.get_center())
            time.sleep(1)
            self.mouse.move_to(self.win.chat.get_center())
            time.sleep(1)

            self.update_progress((time.time() - start_time) / end_time)

        self.update_progress(1)
        self.log_msg("Finished.")
        self.stop()

Test it Out

  1. Launch OSBC.
  2. Proceed to test your bot like normal. Set options and press Play to start the bot.
  3. You should see the mouse move to each major UI element repeatedly.
  4. Press the Stop button to stop the bot.
  5. Change the size of your game window (either in Fixed or Resizable mode) and repeat step 2. This demonstrates how OSBC relocates UI elements on bot start.

Geometry: Points and Rectangles

Everything in OSBC is defined in terms of Points and Rectangles. These are fairly simple data structures defined in src/utilities/geometry.py.

To better understand why we need them, let's look at the code we just wrote:

self.mouse.move_to(self.win.game_view.get_center())

The self.mouse.move_to() function expects a pixel position as input. Pixels on screen can be represented as a tuple of (x, y) coordinates, or a Point object (which is just a glorified tuple that has some extra functionality).

The self.win.game_view property is a Rectangle object. It represents the area of the screen where the world is rendered. The get_center() function returns the center of the rectangle as a Point, which also happens to be the position of the player character.

Rectangles have other useful properties. You can access its width and height, its top & left-most pixels, corner pixels, etc. Most importantly, you can access a random point within the rectangle using the random_point() function, which uses sophisticated randomization to emulate human-like patterns over time. It's also worth mentioning that Rectangles can be screenshotted using the screenshot() function. This is used during image-matching and OCR operations.

Adjust the Bot to Use Random Points

  1. Let's modify our bot to use random points instead of the center of the rectangle.
            # -- Perform bot actions here --
            self.log_msg("Moving mouse to game view...")
            self.mouse.move_to(self.win.game_view.random_point())
            time.sleep(1)
            self.log_msg("Moving mouse to minimap...")
            self.mouse.move_to(self.win.minimap.random_point())
            time.sleep(1)
            self.log_msg("Moving mouse to control panel...")
            self.mouse.move_to(self.win.control_panel.random_point())
            time.sleep(1)
            self.log_msg("Moving mouse to chat...")
            self.mouse.move_to(self.win.chat.random_point())
            time.sleep(1)
  1. Test this code to see the difference. You should see the mouse move to random points within the rectangle instead of the center.

RandomUtil

The RandomUtil module is a collection of functions that can be used to generate random numbers. Most of the utility is used behind the scenes - but on rare occasions, you may need to use it directly.

Let's modify our bot to use the RandomUtil module to take random breaks.

  1. At the top of the file, import the RandomUtil module:
import utilities.random_util as rd
  1. In the main_loop() prior to moving the mouse around, add the following code:
            # 5% chance to take a break between clicks
            if rd.random_chance(probability=0.05) and self.take_breaks:
                self.take_break(max_seconds=15)

In English, this code translates to: "If the user has enabled breaks, there is a 5% chance that we will take a random-length break (no longer than 15 seconds)."

self.take_break() is a function that is defined in the Bot class. It is a simple way to take a random pause while elegantly updating the message log with the break duration. It also makes use of the RandomUtil module.

RuneLite Computer Vision (runelite_cv) & Color Module

Geometry: RuneLiteObject

One of the most powerful features of OSBC is how it takes advantage of RuneLite plugins - namely Object Markers, NPC Indicators, and Ground Items. These plugins allow the bot to identify objects, NPCs, and items on the ground using computer vision. This is done using the runelite_cv module. Many useful functions are already defined in the RuneLiteBot class.

Outlined objects/NPCs can be represented in code as a RuneLiteObject. It functions very similarly to a Rectangle, but the random_point() function will only return a point that is within the irregular outline.

Assuming you are using a fresh OSRS account with access to an axe and tinderbox, let's modify our bot to locate outlined trees.

In the game:

  1. Move your character near normal trees.
  2. Hold Shift and right-click the trees to tag them. The default color is Pink. Tag at least 3 trees.

In your code:

  1. Import the Color module:
import utilities.color as clr
  1. Modify your main_loop() function to look like this:
    def main_loop(self):
        # Setup APIs
        # api_m = MorgHTTPSocket()
        # api_s = StatusSocket()

        # Main loop
        start_time = time.time()
        end_time = self.running_time * 60
        while time.time() - start_time < end_time:

            # 5% chance to take a break between clicks
            if rd.random_chance(probability=0.05) and self.take_breaks:
                self.take_break(max_seconds=15)

            trees = self.get_all_tagged_in_rect(self.win.game_view, clr.PINK)
            if trees: # If there are trees in the game view
                for tree in trees: # Move mouse to each tree
                    self.mouse.move_to(tree.random_point())
                    time.sleep(1)

            self.update_progress((time.time() - start_time) / end_time)

        self.update_progress(1)
        self.log_msg("Finished.")
        self.stop()
  1. Test your bot. You should see the mouse move to each tree in the game view.

Here, we make use of self.get_all_tagged_in_rect(), which is a function defined in the RuneLiteBot class. It needs to know what Rectangle to search within, and what color to search for. The vast majority of colors you'll ever need are defined in the Color module, hence why we use clr.PINK. You can also define your own colors using the Color class.

Finding Nearest Objects

Rarely do we need to know the location of all tagged objects on the screen. Typically, we only care about the one nearest to the player. To find the nearest object, we can use the get_nearest_tag() function. This function is also defined in the RuneLiteBot class. It only requires a Color as input, since it knows to search within the game view.

  1. Modify the tree-searching code in the main_loop() function to look like this:
            if tree := self.get_nearest_tag(clr.PINK):
                self.mouse.move_to(tree.random_point())
                time.sleep(1)

We'll make use of this in the next section.

RuneLite APIs

Another blessing of RuneLite is that there are plugins that expose useful, truthful game state information via HTTP. Right now, there are two API plugins that we can read data from: MorgHTTPClient, and Status Socket. Both do much of the same thing, but Morg is more detailed and has more features, while Status Socket is older and available on more private servers.

They are great for checking if the player is performing animations, if the player is in combat, what items are in the inventory and where, and so on.

In the template bot, both APIs have been added by default at the top of the main_loop() (commented out). It's as easy as typing api_m. to see all the functions available.

Let's modify our bot to chop down a few trees, then drop the logs.

  1. Uncomment the api_m line at the top of the main_loop() function.
  2. Adjust the tree-searching code so that it only executes if the Morg API says that the player is idle.
  3. Add a self.mouse.click() after the mouse moves to the tree.
            # If we are idle, click on a tree
            if api_m.get_is_player_idle():
                if tree := self.get_nearest_tag(clr.PINK):
                    self.mouse.move_to(tree.random_point())
                    self.mouse.click()
            time.sleep(1)

Item IDs

This code isn't great because it'll make our bot chop logs forever. Let's tell it to drop all the logs in our inventory when we have 3 or more.

  1. Import the item_ids module:
import utilities.api.item_ids as ids
  1. Add some code above our tree search to check if we have too many logs. Drop them if we do:
            # If we have 3 or more logs, drop them
            log_slots = api_m.get_inv_item_indices(ids.logs)
            if len(log_slots) >= 3:
                self.drop(log_slots)
                time.sleep(1)

            # If we are idle, click on a tree
            if api_m.get_is_player_idle():
                if tree := self.get_nearest_tag(clr.PINK):
                    self.mouse.move_to(tree.random_point())
                    self.mouse.click()
            time.sleep(1)

What's happening here, is we are asking the API for all of the inventory slots that are holding any type of log. It will give us a list of those slots. If the list is 3 or more, we tell the self.drop() function (exists in Bot class) to drop all of those slots. We then sleep for 1 second to give the game time to register the action.

This is a very simple example of how to use the APIs.

Optical Character Recognition

OSBC has a built-in OCR module that can read text from the game. It is a highly accurate and extremely fast way to read text from the game.

Some functions in Bot and RuneLiteBot make use of this module. For example, self.get_hp() uses OCR to read the HP value next to the minimap. self.pickup_loot() (RuneLite only) uses OCR to read Ground Items text.

This utility has two functions: one for extracting text from a Rectangle, and one for locating specific text in a Rectangle.

Let's use the self.mouseover_text() function to ensure our cursor is hovering over a tree before we click it:

  1. Modify the tree-searching code to look like this:
            # If we are idle, click on a tree
            if api_m.get_is_player_idle():
                if tree := self.get_nearest_tag(clr.PINK):
                    self.mouse.move_to(tree.random_point())
                    if not self.mouseover_text(contains="Chop"):
                        continue
                    self.mouse.click()
            time.sleep(1)

If the word "Chop" isn't in the mouseover text, we skip the click and try again next loop.

Read through src/model/bot.py to see all the functions that make use of OCR.

Image Search

Image searching is useful primarily for locating UI elements that may not be in a static location (E.g., bank deposit button, interface exit button, etc.). It's also useful for private servers that may not have access to the RuneLite APIs.

For demonstration purposes, let's make our bot move the mouse to the tinderbox in our inventory between each iteration using image search.

  1. Lookup Tinderbox on the OSRS Wiki and download the official sprite.
  2. Place this image in the src/images/bot/items/ folder. Call it tinderbox.png.
  3. In your code, import the imagesearch module:
import utilities.imagesearch as imsearch
  1. At the top of the main_loop() function, add the following:
        # Get the path to the image
        tinderbox_img = imsearch.BOT_IMAGES.joinpath("items", "tinderbox.png")
  1. After the tree search code, add the following:
            # Move mouse to tinderbox between each iteration
            if tinderbox := imsearch.search_img_in_rect(tinderbox_img, self.win.control_panel):
                self.mouse.move_to(tinderbox.random_point())

Found images are treated as Rectangles, with the exact dimensions of the input image. OSBC's image search function works with transparency, which is why it works with the official tinderbox sprite.

Final Code

import time

import utilities.api.item_ids as ids
import utilities.color as clr
import utilities.random_util as rd
from model.osrs.osrs_bot import OSRSBot
from utilities.api.morg_http_client import MorgHTTPSocket
import utilities.imagesearch as imsearch

class OSRSTutorial(OSRSBot):
    def __init__(self):
        bot_title = "Tutorial"
        description = "<Bot description here.>"
        super().__init__(bot_title=bot_title, description=description)
        # Set option variables below (initial value is only used during UI-less testing)
        self.running_time = 1
        self.take_breaks = True

    def create_options(self):
        """
        Use the OptionsBuilder to define the options for the bot. For each function call below,
        we define the type of option we want to create, its key, a label for the option that the user will
        see, and the possible values the user can select. The key is used in the save_options function to
        unpack the dictionary of options after the user has selected them.
        """
        self.options_builder.add_slider_option("running_time", "How long to run (minutes)?", 1, 500)
        self.options_builder.add_dropdown_option("take_breaks", "Take breaks?", ["Yes", "No"])

    def save_options(self, options: dict):
        """
        For each option in the dictionary, if it is an expected option, save the value as a property of the bot.
        If any unexpected options are found, log a warning. If an option is missing, set the options_set flag to
        False.
        """
        for option in options:
            if option == "running_time":
                self.running_time = options[option]
            elif option == "take_breaks":
                self.take_breaks = options[option] == "Yes"
            else:
                self.log_msg(f"Unknown option: {option}")
                print("Developer: ensure that the option keys are correct, and that options are being unpacked correctly.")
                self.options_set = False
                return
        self.log_msg(f"Running time: {self.running_time} minutes.")
        self.log_msg(f"Bot will{' ' if self.take_breaks else ' not '}take breaks.")
        self.log_msg("Options set successfully.")
        self.options_set = True

    def main_loop(self):
        """
        When implementing this function, you have the following responsibilities:
        1. If you need to halt the bot from within this function, call `self.stop()`. You'll want to do this
           when the bot has made a mistake, gets stuck, or a condition is met that requires the bot to stop.
        2. Frequently call self.update_progress() and self.log_msg() to send information to the UI.
        3. At the end of the main loop, make sure to set the status to STOPPED.

        Additional notes:
        Make use of Bot/RuneLiteBot member functions. There are many functions to simplify various actions.
        Visit the Wiki for more.
        """
        # Setup APIs
        api_m = MorgHTTPSocket()

        tinderbox_img = imsearch.BOT_IMAGES.joinpath("items", "tinderbox.png")

        # Main loop
        start_time = time.time()
        end_time = self.running_time * 60
        while time.time() - start_time < end_time:

            # 5% chance to take a break between clicks
            if rd.random_chance(probability=0.05) and self.take_breaks:
                self.take_break(max_seconds=15)
            
            # If we have 5 or more logs, drop them
            log_slots = api_m.get_inv_item_indices(ids.logs)
            if len(log_slots) >= 3:
                self.drop(log_slots)
                time.sleep(1)

            # If we are idle, click on a tree
            if api_m.get_is_player_idle():
                if tree := self.get_nearest_tag(clr.PINK):
                    self.mouse.move_to(tree.random_point())
                    if not self.mouseover_text(contains="Chop"):
                        continue
                    self.mouse.click()
            time.sleep(1)

            # Move mouse to tinderbox between each iteration
            if tinderbox := imsearch.search_img_in_rect(tinderbox_img, self.win.control_panel):
                self.mouse.move_to(tinderbox.random_point())

            self.update_progress((time.time() - start_time) / end_time)

        self.update_progress(1)
        self.log_msg("Finished.")
        self.stop()