-
Notifications
You must be signed in to change notification settings - Fork 141
Creating Your First OSRS Bot
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.
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.
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.
- In the
src/model/osrs/
folder, duplicate thetemplate.py
file and rename it to something meaningful. For example,my_bot.py
. - 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)
.
- 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:from .my_bot import OSRSMyBot
- Run the
src/OSBC.py
file to display the UI. You should see your bot listed under the OSRS tab. - Close the UI for now.
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.
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
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
- Run the
src/OSBC.py
file to display the UI. - In the navigation pane on the left, click the dropdown menu and select "OSRS".
- 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.-
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 theprofiles2
folder, not the.runelite
folder.
-
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
- In the game, log in to your account.
- Back in OSBC, select your bot from the list on the left.
- Open the
Options
panel and set the bot to run for 1 minute. - In the OSBC UI, click the
Play
button to start the bot. Hover over the button to see a key-bound shortcut.- Watch the UI for the
Hello World!
message every second. - Note the progress bar increasing over time.
- Watch the UI for the
- Click
Stop
to stop the bot. - Close the UI. You can keep RuneLite running.
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.
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"])
- Open the
src/model/osrs/my_bot.py
file. - In the
__init__()
function, add a variable that will store the user selection from the dropdown menu. We will call itself.take_breaks
. Give it a default value ofTrue
. - 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"])
- 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.
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.
- In the
save_options()
function, add a line of code that sets theself.take_breaks
variable to the value of thetake_breaks
option. You can do this by using theoptions
dictionary that is passed to the function. Theoptions
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.
- Test the bot again. You should see a message saying options were set successfully.
We will make use of this option in later sections.
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.
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.
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.
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.
- 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()
- Launch OSBC.
- Proceed to test your bot like normal. Set options and press
Play
to start the bot. - You should see the mouse move to each major UI element repeatedly.
- Press the
Stop
button to stop the bot. - 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.
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.
- 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)
- Test this code to see the difference. You should see the mouse move to random points within the rectangle instead of the center.
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.
- At the top of the file, import the
RandomUtil
module:
import utilities.random_util as rd
- 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.
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:
- Move your character near normal trees.
- Hold
Shift
andright-click
the trees to tag them. The default color is Pink. Tag at least 3 trees.
In your code:
- Import the
Color
module:
import utilities.color as clr
- 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()
- 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.
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.
- 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.
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.
- Uncomment the
api_m
line at the top of themain_loop()
function. - Adjust the tree-searching code so that it only executes if the Morg API says that the player is idle.
- 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)
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.
- Import the
item_ids
module:
import utilities.api.item_ids as ids
- 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.
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:
- 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 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.
- Lookup
Tinderbox
on the OSRS Wiki and download the official sprite. - Place this image in the
src/images/bot/items/
folder. Call ittinderbox.png
. - In your code, import the
imagesearch
module:
import utilities.imagesearch as imsearch
- 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")
- 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.
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()