Welcome to the dark side.
Published: 2021-11-20 04:20:33

We all hear about the dark side of the web. The truth is, this can be in many contexts. 

As a young kid, I used to love playing Runescape. Even as I got older and found extra time, I played a bit and was still always following the rules, avoiding botting, and you know - grinding my 99 strength skill cape in NMZ. 

![nmz meme : r/2007scape](https://i.redd.it/bjco3692xepy.png)

I realized earlier in the semester, a fellow student was contributing to a bot on GitHub for Old School Runescape, and after some discussion I was eager to contribute as well. 



I had some concerns about my contribution, since the codebase is in Python, and I have never written in Python. 

After some thinking, I was going to create a cooking script for wines - something easy to start. But I saw that they wanted a wood cutting script, and I thought wow this is actually much easier, since you just click the tree to cut, and then drop the logs. 

So technically there are two tasks: 

1. Find and click the tree
2. Once the inventory is full, drop the logs. 

There seems to be a recurring theme for being SOL every time I pick a repository to contribute to, and as you guessed it, strap your seatbelts because..... HERE WE GO AGAIN. 

The core of the bot program uses pyAutoGUI. This means that the script grabs a reference of your client, and then looks for items matching that in the client. The bot requires you to have your orientation to point N, zoomed fully in, fixed screen mode, point your camera as far up, and full brightness, so it can grab accurate references of images in the game screen.

For example, lets discuss the dropping logs task. As you can see below, that is the client and what puAutoGUI will see.

![](https://i.imgur.com/94ehAn6.jpeg)

When I send the following code: 

```python
  logs_in_inventory = vis.Vision(
            region=vis.INV, needle=self.log).count_needles()
```

It's going to use the reference of self.log which is actually just this file: 

![](https://i.imgur.com/2OMkmIt.png)

and count all the instances of it. So for this example it would return 14. 



The main hurdle for this contribution was the first task: Find a tree and click on it. The code to find a picture and click on it is there - so why is it hard? I overlooked the fact the trees are all rotated at different angles. I tried my best to overcome this, and thought if I grab a sample small enough to not matter, but big enough to capture on screen it will work - but it doesn't. I got back to the issue and commented about my findings. The repository owner asked that I explore the pyAutoGUI function for grabbing by pixel color. This is what I came up with for the function.

```python
    def find_pixel_by_color(self, colors):

        # 1. Grab a reference of the game screen as a screenshot  
        needle = pag.screenshot(region=self.region)   
        pixel_coords = None
        # 2. In that screenshot, search w and h to find pixel matching that color and click it.
        for x in range(needle.width):
            for y in range(needle.height):
                if needle.getpixel((x, y)) in colors:
                    pixel_coords = (GAME_SCREEN[0] + x, GAME_SCREEN[1] + y, 5, 5)    # Stored pixel coords
                    # log.info("pixel color: %s", needle.getpixel((x,y)))
                    # log.info("pixel found %s,", pixel_coords)
                    
                    return pixel_coords
        return False
```



The code was easy enough right? However, there was a few problems.

As you can see from the one screenshot - there are MANY things which are green, including the tree. To grab an accurate color which is only present in the OAK trees, was very difficult. I eventually took pictures of the trees, and used a tool online to map out the colors in the picture. 

This is the colors from the regular tree:

![](https://i.imgur.com/iAws9hl.png)



This is the colors from the Oak tree: 

![](https://i.imgur.com/ohi8Mot.png)

All the colors are actually there, even if the percentage is 0.00000, the color is technically still there when I try to click on it. So what I did was I tried a few trees, and grabbed some colors which did not pop up in the regular trees too often: 

```python
    # Is going to look for a tree, find the coordinates, and send back the coordinated for the pixel.
    def find_tree(self):
        if self._is_inventory_full() is True:
            log.info("Inventory is full!")
            self.drop_inv_log()

        # Some of the colors from oak trees
        colors = [(96, 120, 48), (137, 146, 97),
                  (165, 172, 120), (145, 158, 86)]

        # Look for tree pixel
        tree_coords = vis.Vision(
            region=vis.CLIENT, needle='').find_pixel_by_color(colors)

        # Stop script if we don't see pixel
        if tree_coords is False:
            log.info("No trees in area, stopping script.")
            return False
        log.info("Tree spotted")

        # inputs.Mouse(region=tree_coords).click_coord()

        return tree_coords
```

This was good, because it was consistent enough, and the repository owner had already said at the very least "if I stand in front of the tree, it will cut it and drop the logs". 

So now, I had the script to cut the tree, and so I cleaned up the functions to check if the inventory had any logs, and to empty the inventory. Those were simple. 

The next challenge was understanding how to write the code properly so that it would stop clicking the pixel each 2 seconds, but would also stop clicking and wait for the pixel when the tree disappeared after being cut. 

This is another thing I have to explain - When you cut oak trees, you may receive a log, but the tree may still be up there (it has more logs to cut off, before it disappears and respawns). So if I click on where the pixel was, my character will actually move to that spot, and might walk off far enough that the oak tree is out of the view. 

From this I understood I had to keep clicking the tree, while the pixel was present, and then wrote it so that it checks every 20 seconds (very timely for the functionality - after running a few time tests)

```python
    # This function iterates over the tree needle to cut it

    def cut_tree(self, tree_coords) -> bool:
        tree_coords = self.find_tree()
        while not tree_coords:
            misc.sleep_rand(0,2000)

        tree_clicked = inputs.Mouse(region=tree_coords).click_coord()

        return tree_clicked
```

```python
def woodcut(loops: int = 10000):

    log = "./needles/items/oak_log.png"

    for _ in range(loops):
        # Grab reference of the pixel first. 
        tree_coords = skills.Woodcutting(log=log).find_tree()
        # Try and click the tree
        while skills.Woodcutting(log=log).find_tree() != False:
            tree_clicked = skills.Woodcutting(log=log).cut_tree(tree_coords=tree_coords)
            # if successful give it a time to chop, and check again
            if tree_clicked != False:
                misc.sleep_rand(0,20000)


```


My account will probably get banned for botting soon, and I would have gotten my 30 wood cutting for nothing, but I implemented **two** new features (get_pixel_by_color and woodcutting script), **and** I wrote my first python program.