Adding Functionality - Looting
In this tutorial we will be adding looting functionality to our cow killer script, we will then bank them in the next tutorial! We will be killing at lumbridge, and banking at lumbridge. We will move onto more advanced concepts, so make sure you read everything fully.
Step One - Create a new task “Loot”
The first thing we need to do is to create a new task to house our looting feature, and then we need to add this task to our tasks ArrayList. Easy! As we are now in the fifth tutorial of this series you should have a good idea of how to do what I have just said, and as we move on through this series I need to stop putting steps we have already covered in great detail, I am confident that if you followed the previous tutorials you can do what I have just said, but here is an overview if you’re stuck.
Create a new Java Class within our tutorial package, name this Loot. Inside of Loot it needs to “extends Task” as it will be a task. Do alt+enter on the errors to implement the methods and constructor that we need.
See image for reference: https://i.imgur.com/YQd5VvE.png
So, now we just need to create a Loot object named loot and add it to our tasks ArrayList.
See image for reference: https://i.imgur.com/FCwKqlY.png
Awesome, you’ve just adding looting to your script (ok ok, it won’t actually loot - but let’s go code that bit).
Step Two - Coding our Loot task
Ok, step 1, let’s just go into the game and kill a few cows and observe how we personally would loot. Ok, so I observed 2 things. I personally just killed a cow and picked its hide up, however I found it faster to kill a cow, kill another cow, then loot them both. Depending on how things go we may look into trying this 2nd method.
While we’re in the game, let’s press View->Ground Items, as we need the cowhide ID so we can loot it. (It’s 1739).
See image for reference: https://i.imgur.com/vwGJc7e.png
So, first thing we do is similar to when we found our cows, make a variable and store our cowhide ID. This variable will NOT be an array as cowhide ID will never change, so it will just be an int named cowhide with a value of 1739. This is inside of our Loot class.
See image for reference: https://i.imgur.com/0aCsN34.png
Now, in order to stop us from looting other peoples loot (we only want to loot the cows we kill) we need to consider where the cow is that we are attacking and then limit our looting to that spot. The way I am going to try and achieve this is to put a piece of code within our activate() that stores the Tile of the thing we are interacting with. A Tile (class) is a class that holds an XYZ coordinate that corresponds to a spot in the game.
Since we will be using this tile within our execute() method also, we can NOT put the variable in activate so it must be put below our cowhide ID at the top. This will be a variable with datatype of Tile named cow_tile and a value of Tile.NIL (remember what nil is from our previous tutorial).
See image for reference: https://i.imgur.com/CBInVtm.png
So, one important thing to consider is that although our activate() method is only something that checks if we execute() or not, we can still put code within activate() and it will still be ran. So inside of our activate() method we want to update cow_tile to the tile of the cow we’re fighting, providing it is valid (will not be valid if we’re not fighting anything).
So this will read something like: if we are fighting something, cow_tile = tile of the thing we are fighting. See if you can figure that code out, I believe you can if you got this far!
See image for reference: https://i.imgur.com/TL5x00F.png
If you didn’t get it by yourself don’t worry, just make sure you are fully understanding everything as we go. Interacting() hands us the NPC that we are interacting with, and valid() checks that it is a valid NPC. Remember from the last tutorial that the RSBot API is null safe so it will never return null (empty) instead it will return an invalid NPC or object.
Now, what will our activate() code? Well, if there’s a cow hide on that Tile let’s go get it! We have not handled Tiles or GroundItems before so follow along. Use the code I have written below and then I will explain it:
See image for reference: https://i.imgur.com/loelFh3.png
Ok, what this code says is:
ctx.groundItems = we are targeting ground items
select() = refresh the list, give me the latest data
at(cow_tile) = only check for items that are at the same tile as cow_tile
id() = only with the same ID as a cowhide (it is a cowhide)
isEmpty() = returns true or false depending on if the list is empty or not (it will be empty IF there are no cowhides on that tile)
HOWEVER notice the ! at the start, that inverses it. So we are actually checking that it is NOT empty, so it returns true IF there are cowhides at that tile.
And now, for our execute() code. We actually want to use the same code to select the cowhides, however this time we want to simply interact with it (Take) to pick it up. See if you can write that code yourself and compare with mine
See image for reference: https://i.imgur.com/wiFTJhO.png
You probably got it a little different but that’s fine, but change it to mine for now so we are at the same place and I will explain. What my code does is stores the cowhide we want to TAKE as a variable named hide. Then we say hide.interact() and we put “Take” however as there will be multiple things with the take action (bones and meat) we specify a second parameter hide.name() this will make it look for “Take Cowhide” rather than just Take. This is good practice.
The problem we have here is that if we were to run this script, it will definitely click the cowhide 100 times while it tries to pick it up, so we want to add a sleep. We were introduced to sleeps previously but let’s take it to another level. Let’s use a dynamic sleep which will sleep for an optimal amount of time rather than just a set amount of time.
To do this we must use Condition.wait(). This will probably show you an error. This is similar to when we used the filter earlier, but .. different.. So, alt+enter on the error (import the Condtion class) and then alt+enter on the second error on the .wait() part, and introduce the variable. Make sure you click between the brackets when you press alt+enter.
See image for reference: https://i.imgur.com/e7wiRhc.png
See image for reference: https://i.imgur.com/TowzTt9.png https://i.imgur.com/XmWJJLE.png
So, we still have errors. But as you can see it generated a variable for us of type Callable<Boolean> named booleanCallable. We could rename this but it isn’t important. What this variable wants to do for us is check whether we have finished looting.
So, let’s initialize this variable. To do this we need to assign it a value. Since it is a Callable<Boolean> we type new Callable<Boolean>(); Make sure you select the right one from the suggestive list.
See image for reference: https://i.imgur.com/vShXTOM.png
So currently we see this has now generated us a method and such, but it is returning null currently.
See image for reference: https://i.imgur.com/xvgLOsz.png
What we want to do is make it return true when we have looted, the best way for us to do this is simply to check if hide.valid() is not true (it will become invalid when it has been taken). Can you remember how to do this? Remember to use ! to inverse something.
See image for reference: https://i.imgur.com/GMMwBDB.png
Now, to fix our last error we just need to add our missing ; from the line.
See image for reference: https://i.imgur.com/3Id3sbo.png
Finally, wait() can accept up to 3 parameters. We will be using all 3. The first one we have already provided it (booleanCallable), the second is frequency, and the third is tries.What this means is we can tell it how long to wait before each check (frequency) and how many times to try before giving up. Looting is fairly quick, but on a slow machine or if you’re far away let’s say it could take up to 3 seconds. So let’s use 300ms as our frequency, and try 10 times.
See image for reference: https://i.imgur.com/ySQhN38.png
So to clarify, what that Condition.wait() does is rather than sleeping for 2500ms it sleeps for 300ms and checks and repeats UP TO 10 times. It may only take 1 time to complete, so it may only sleep 300. This makes our script much more robust and efficient.
I’m going to run my code now and see where we are up to. You can do the same if you wish. I am coding this for the first time just like you so let’s expect some errors. Remember to rebuild your project if you already have RSBot open.
Ok, first things first - I needed to edit our Fight class. At the end of the last tutorial I had you add some code, this code was incorrect. EDIT: I have actually fixed this in the last tutorial now. (SORRY).
See image for reference: https://i.imgur.com/ff0SXrq.png
Once you make that change you will find that your script runs, but never loots. This is because the cows tile is actually not the same tile that it drops its loot. I used a simple print statement to debug this:
See image for reference: https://i.imgur.com/PrWncY4.png
I then ran the script and it printed out the cow_tile, I then manually went to the tile with the loot on and compared the 2. It appears that the cow_tile is the cows front legs, and the loot is landing where it’s back legs are. So, this is now a bit tricky.
The best way I can think of without overdoing it is to consider the maximum size of the cow and check all possible tiles around cow_tile for loot. We could overcomplicate this and work out the exact tile using the cows orientation and length, but this will be much easier.
For game objects the length of the item is actually stored in a method, but for NPCs this is not the case, so we'll just have to know that cows are 2 tiles in size at their maximum.
Here is what I have came up with, sadly this would have been much better as a video so you could watch me work through the problem but I will have to just talk you through the code.
I created a new variable of type Area named cowLootArea. An area is a collection of tiles which you create using the top left corner and the bottom right (to create a square).
To create my area I used my cows tile +2 for top left and -2 for bottom right.
I wrote this code in my activate()
I changed my at(cow_tile) calls to within(cowLootArea) to see if the loot is within this area.
See image for reference: https://i.imgur.com/F2iUdli.png
There isn’t much to for me to talk about there as they are just things you pick up as you do things. If there is anything you don’t understand please leave a comment and I can come back and update this for you.
Now! We still have issues, our script is too fast and will kill the next cow before the loot appears, this is kind of good as it’s faster - but then it does not come back to get our loot. So let’s allow multiple loot tiles! Let’s simply change our cowLootArea to an ArrayList, so it can store multiple areas. Then we can keep track of multiple cows.
I will make a few minor improvements while we do this. First of all I am going to perform a null check in our activate() to check that cowLootArea is not null. This is important as by default we specified our cowLootArea=null so this could cause issues in some situations if we do not check.
See image for reference: https://i.imgur.com/5YnPnbG.png
Next, I want to move our cowLootArea code into the part above where it checks that we are interacting with a valid npc, as there is no use creating this area if we are interacting with an invalid npc…
See image for reference: https://i.imgur.com/BN2Ohn9.png
Now I can work on making this into an ArrayList. First thing to do is to change our data type for this variable from Area to ArrayList of Area’s (ArrayList<Area>) We also need to initialize this using = new ArrayList<Area>;
See image for reference: https://i.imgur.com/b0k9xPw.png
Now, inside of our activate() rather than putting cowLootArea = we want to add it to our ArrayList, so we would use add(). I did have to add an additional bracket at the end before; to make this valid.
See image for reference: https://i.imgur.com/d0yP1VX.png
Now, we don’t want to add the same area over and over again. Rather than check our ArrayList every time, I am going to add some code to see if the cows tile has changed since last time - if it hasn’t, the area we create would be the same.
See image for reference: https://i.imgur.com/pegKE2O.png
Finally, we can’t use within() with an ArrayList, so we are going to have to loop through our ArrayList to perform this check. We covered loops previously, we will be using the same type of loop - for.
I will be adding a boolean called lootExists and setting this to be false by default. This is inside of our activate() method. We will set this to true if loot exists. If loot does exist then we will be “breaking” from our loop, this is because we only care if loot is in 1 of the areas, so we do not bother checking them all at this point.
Finally we must change our activate() return statement to check if the area is null, and if lootExists.
See image for reference: https://i.imgur.com/kRGuk8P.png
Now, let’s modify our execute(). First thing we do is make a copy of our cowLootArea List, because we can’t modify a list while we are reading it. But we may want to cross an area off our list (when we loot it). So we make a copy to read from, then cross off the original list.
See image for reference: https://i.imgur.com/4p0gEly.png
Now, let’s loop through our copied list and check for loot.
See image for reference: https://i.imgur.com/32GqYAc.png
Now, if there is loot we want to take it. We already have code for this so just move our code from earlier inside of this if. Remember to change within(cowLootArea) to within(a).
See image for reference: https://i.imgur.com/z3CSBQn.png
Finally, we need to remember to remove this from the list. This is as simple as cowLootArea.remove(a)!
See image for reference: https://i.imgur.com/tSVCdwC.png
Oh, also let’s check we aren’t fighting a cow before we try to loot. Add that to your activate and compare below!
See image for reference: https://i.imgur.com/GmdnfNn.png
I decided to use the inverse (!) valid() check as this seems most reliable.
That’s our looting done. However, remember that 2500ms sleep in our fight class? Well, if we kill the cow within 2.5 seconds it means our loot task would never note down the tile (because our script is busy sleeping). We learnt earlier about dynamic sleeping, so let’s use that there. Let’s try to sleep until we are interacting with the cow.
Do this within your Fight class. We will use 100 frequency and 25 tries, so a maximum of 2500 still - but likely quicker.
See image for reference: https://i.imgur.com/4h0itzG.png
Ok, so now if you run this it will probably work well for a minute and then crash! If we look at the logs we see this:
See image for reference: https://i.imgur.com/TJqPAsB.png
This tells me at line 51 we have a ConcurrenctModificationException. Remember when I told you that we need to make a copy of the list? Well, we did it wrong! I left this in because it’s normal to do things wrong, and it was a genuine oversight on my part. Essentially it’s saying we are modifying the list while reading it - but what? We made a copy! Well, we didn’t actually make a copy. We make a variable that has the value of our other variable. They are essentially the same thing, but with a different name. So, to get around this rather than making a copy of the ArrayList, let’s use the original - this means we not loop with for(Area a : cowLootArea)
See image for reference: https://i.imgur.com/getRx6P.png
Now, let’s make a new ArrayList<Area> called toRemove. Can you guess what we will do? Probably! We will add the ones we want to remove to this list, then remove them all once we’ve finished reading the list.
So rather than cowLootArea.remove() we will use toRemove.add() and then after we finish our loop we can just remove them all from cowLootArea (cowLootArea.removeAll(toRemove);
See image for reference: https://i.imgur.com/7TCb1cE.png
Finally, I’m going to just go back and modify my activate code as I noticed if a cow was moving when we attack it, it would actually add lots of areas causing issues. I am checking we are right next to the cow.
See image for reference: https://i.imgur.com/cscJ0IG.png
Finally again I found it really useful to keep track of my areas just using simple debugging. Here’s what I added to my code:
See images for reference: https://i.imgur.com/bYbXUPu.png https://i.imgur.com/hpOE2Gv.png
If we attack a cow that is moving we end up adding multiple areas still, this is a simple fix - if the cow is moving then so are we, so let’s make sure we are stationary before we add the area to our list.
See image for reference: https://i.imgur.com/GRbobhb.png
Ok that still gave some issues, I found the following was the best results
See image for reference: https://i.imgur.com/j9OhEUx.png
This still sometimes adds 2 areas instead of 1, however it’s the best I am going to get I think. Feel free to play yourself and let me know what you come up with!
Step Three - Some improvements
So, this tutorial was supposed to be rather simple. When I started writing it I did not expect to have to utilise Area’s or anything, I just thought it would be as simple as storing the Tile - this would be the case for 1 Tile NPCs but not for our big boy cows. I have slept since writing the above and have 1 thing that I want to add which may improve the script. What I figured is what if in our Fight class we change our sleep to wait until we are actually attacking the cow and have stopped moving, it would probably make our storing of Areas more accurate (because the Loot task can’t do anything until we finish sleeping). So let’s take a look.
See image for reference: https://i.imgur.com/nigYJI2.png
So as you can see here I have only added && !ctx.players.local().inMotion()
What this code says essentially is that (notice the inverse at the start.. !) if our player is in motion (inMotion literally just checks that our speed is 0, but it’s easier to write than .speed() == 0 in my opinion. So it checks if we are moving, and inverses it. So it will not sleep until we aren’t moving AND we are interacting with a valid cow.
Let’s run this now and see if we have any improvement.
I don’t know about you but that is running so much better for me. I am happy I slept on this before posting it!
One other thing we need to quickly do is add a check to our activate(), we need to check that our inventory is not full in our Loot task. We have to just use ctx.inventory and look for something relevant (isFull()) and inverse it using !
See image for reference: https://i.imgur.com/Vg9Wdan.png
Step Four - Something to copy
When we encountered the problem earlier about having to create Areas I mentioned that the other way to do it would be to use the cows orientation to and length to calculate the exact tile, and then to use that instead of creating Area’s. I was going to code this but in doing so I realised it was not complicated at all, the loot fell at X-1 and Y-1 of the cows tiles every time regardless of orientation. There are quite a few changes I will talk through, but replace your Loot code with mine at the link below.
Essentially I have not actually changed that much, but let’s go through.
First of all I renamed cowLootArea to cowLootTile, and changed it from an ArrayList of Area’s to an ArrayList of Tiles.
See image for reference: https://i.imgur.com/8oNReFo.png
Then what I had to do was to make a variable to hold our cow, the datatype of this was ofcourse Npc, however on the right hand side of the equals you may be confused why there is an (Npc). I can’t remember if we covered this already but this is called a Cast.
In RuneScape we could be interacting with an Npc, or a Player. Therefore they have a common class called an Actor. Npc extends actor, and Player extends Actor, so they share a lot of the same methods. But when we ask for what we are interacting with, it can only guarantee an Actor, because it has no context. We know we aren’t going to interact with players, so we can personally guarantee that, and so we can use (Npc) to order it to convert this to an Npc instead.
Then we create our Tile variable named newTile, and give this the value of a Tile that we specify the coordinates of. We know that the loot falls at -1 Y and -1 X of the cows Tile. (I found this out by printing out the cows tiles and then comparing this to where the loot fell ingame.
Finally, I just add this newTile to our array of tiles.
See image for reference: https://i.imgur.com/MxIzH5f.png
Now, I change our loop to go through Tiles instead of Areas.
See image for reference: https://i.imgur.com/fpfeN6H.png
And then in our execute() we do the same, change to loop through tiles.
See image for reference: https://i.imgur.com/8cmdEt0.png
And that’s actually it. This is much better because as I said before, an Area is a collection of Tiles, so everytime we checked our area we checked multiple tiles. Now, we only check 1 tile per loot tile! Yay!
So, yes - I could have just edited this tutorial and removed the parts with the Areas, but again - this is a teaching experience. This isn’t a race to complete a script, I am trying to teach you things to allow you to get going and create your own things. You now know about Areas, we aren’t using them here but you know how to!
See you in the next tutorial!