- Joined
- Dec 10, 2014
- Messages
- 3,255
- Thread Author
- #1
An Introduction to the State-based Script!
By: SlashnHax
Contents:By: SlashnHax
- Introduction
- Overview
- Pros and Cons
- Pros
- Cons
- Creating the Script
- The Skeleton
- The Manifest
- Defining the States and implementing getCurrentState()
- Implementing the methods
- Refining the methods
- End product
- Parting words
G'day mates. Welcome to my tutorial: An Introduction to the State Script. Throughout this tutorial we will cover the basics of the State-based Script and create a functional Script (an iron powerminer) using this approach.
Overview
A basic tutorial covering the State Script. The Pros and Cons of using this approach are discussed, then we move into creating a functional example by working through the required processes.
Pros and Cons
Pros
Creating the Script- All of your code is in the one place. This is perfect for simple, small scripts where there isn't much code.
- State-based scripts are pretty straightforward, making them perfect for your first script.
- You can't reuse code as easily as you can with a Task-based script.
- Large projects become messy and hard to read.
The Skeleton
Replace StateSkeleton with the name you're going to save the file as, without the .java extension. (e.g. PowerMiner)
The import statement at the top imports the LoopingScript class, which is the type of Script we will be using. The onLoop() statement is called over and over until the Script is stopped.
We will need to import more classes later on, but for now this is all we need.
The Manifest
Refining the methods
Code:
import com.runemate.game.api.script.framework.LoopingScript;
/**
* Skeleton for a State-based Script
* Created by SlashnHax
*/
public class StateSkeleton extends LoopingScript {
private enum State{
}
@Override
public void onStart(String... args){
}
@Override
public void onLoop() {
}
@Override
public void onStop(){
}
private State getCurrentState(){
return null;
}
}
The import statement at the top imports the LoopingScript class, which is the type of Script we will be using. The onLoop() statement is called over and over until the Script is stopped.
We will need to import more classes later on, but for now this is all we need.
Covered by Cloud in the official post. -link-
My manifest for this project (your main-class tag will be different):
Defining the States and implementing getCurrentState() (*1)My manifest for this project (your main-class tag will be different):
Code:
<manifest>
<main-class>com.slashnhax.tutorials.statescripttutorial.PowerMiner</main-class>
<name>Tutorial PowerMiner</name>
<description>Powermines iron.</description>
<version>1.0</version>
<compatibility>
<game-type>RS3</game-type>
</compatibility>
<categories>
<category>MINING</category>
</categories>
<!--Required to publish on the bot store-->
<internal-id>TutorialPowerminer</internal-id>
<!--The rest are optional-->
<hidden>false</hidden>
<open-source>true</open-source>
</manifest>
We already have the State enum from the skeleton, but now we need to populate it with the States that our script is going to use.
If you don't understand enums or want to brush up your knowledge -here is the official java enum tutorial-
For now, our miner shall function as a powerminer, so we need to think of what is done while powermining: Mining, waiting and dropping. We add these to the State enum resulting in:
Now we must implement getCurrentState(). The purpose of getCurrentState() is to determine which State we are currently in, allowing us to act accordingly in our onLoop().
We want to either drop when we have a full inventory, mine when we are idle or wait while we aren't idle. To translate this into code we must first work out what determines whether our Player is idle or not, which is when their animation is -1 (*2). Determining if we have a full inventory or not is pretty easy, as there is a method for it in the Inventory API.
Therefore our pseudocode for getCurrentState() is:
Which makes our actual implementation:
Your current code should resemble this:
Implementing the methodsIf you don't understand enums or want to brush up your knowledge -here is the official java enum tutorial-
For now, our miner shall function as a powerminer, so we need to think of what is done while powermining: Mining, waiting and dropping. We add these to the State enum resulting in:
Code:
private enum State{
MINE, WAIT, DROP;
}
We want to either drop when we have a full inventory, mine when we are idle or wait while we aren't idle. To translate this into code we must first work out what determines whether our Player is idle or not, which is when their animation is -1 (*2). Determining if we have a full inventory or not is pretty easy, as there is a method for it in the Inventory API.
Therefore our pseudocode for getCurrentState() is:
Code:
private State getCurrentState(){
if(full inventory){
return DROP;
} else if (player is idle){
return MINE;
} else {
return WAIT;
}
}
Imports (these go up the top with the other import):
getCurrentState():
Code:
import com.runemate.game.api.hybrid.local.hud.interfaces.Inventory;
import com.runemate.game.api.hybrid.region.Players;
Code:
private State getCurrentState(){
if(Inventory.isFull()){
return State.DROP;
} else if (Players.getLocal().getAnimationId() == -1){
return State.MINE;
} else {
return State.WAIT;
}
}
Code:
import com.runemate.game.api.hybrid.local.hud.interfaces.Inventory;
import com.runemate.game.api.hybrid.region.Players;
import com.runemate.game.api.script.framework.LoopingScript;
/**
* Skeleton for a State-based Script
* Created by SlashnHax
*/
public class PowerMiner extends LoopingScript {
private enum State{
MINE, DROP, WAIT;
}
@Override
public void onLoop() {
}
private State getCurrentState(){
if(Inventory.isFull()){
return State.DROP;
} else if (Players.getLocal().getAnimationId() == -1){
return State.MINE;
} else {
return State.WAIT;
}
}
}
Now we have our States and our getCurrentState() implementation, we can fill in our onLoop() method with the code needed to make the script work as we want it to. We manage this buy using a switch statement -here is the official java tutorial- and the State enum we've defined. In our switch statement we have our 3 cases, the States we defined earlier. Now we work out what we want to do in each of these cases.
case MINE:
case WAIT:case MINE:
Here we want to do two things: Find the nearest Iron ore rocks and Mine them. To do this we need to import the GameObject and GameObjects classes.
To get the nearest Iron ore rocks we will use a query to select the objects by name, then get the results and select the nearest one, we will then store it as a variable.
Now that we have our rocks we want to mine them. First we want to make sure they aren't null, then we want to mine them using .interact(String action) method:
case DROP:
Code:
import com.runemate.game.api.hybrid.entities.GameObject;
import com.runemate.game.api.hybrid.region.GameObjects;
Code:
GameObject rocks = GameObjects.newQuery().names("Iron ore rocks").results().nearest();
Code:
if(rocks != null){
rocks.interact("Mine");
}
Here we have a couple of choices: We can drop everything or only drop certain items. For now we will drop everything.
For our dropping, we will use a for-each loop to iterate through the items in the inventory and "Drop" each of them then wait for a small amount of time. To do this we need to import SpriteItem, which is what the items of the inventory are, and Execution, which handles the delay.
Then we will use a for-each loop to loop through the items, dropping them and waiting 500ms to 1000ms before moving onto the next one:
For our dropping, we will use a for-each loop to iterate through the items in the inventory and "Drop" each of them then wait for a small amount of time. To do this we need to import SpriteItem, which is what the items of the inventory are, and Execution, which handles the delay.
Code:
import com.runemate.game.api.hybrid.local.hud.interfaces.SpriteItem;
import com.runemate.game.api.script.Execution;
Code:
for(SpriteItem i:Inventory.getItems()){
i.interact("Drop");
Execution.delay(500, 1000);
}
Here we do stuff we want to do while waiting, or nothing. For now we'll do nothing.
Our (barely) functional script so far:
Code:
package com.slashnhax.tutorials.statescripttutorial;
import com.runemate.game.api.hybrid.entities.GameObject;
import com.runemate.game.api.hybrid.local.hud.interfaces.Inventory;
import com.runemate.game.api.hybrid.local.hud.interfaces.SpriteItem;
import com.runemate.game.api.hybrid.region.GameObjects;
import com.runemate.game.api.hybrid.region.Players;
import com.runemate.game.api.script.Execution;
import com.runemate.game.api.script.framework.LoopingScript;
/**
* PowerMiner for RuneMate tutorial
* Created by SlashnHax
*/
public class PowerMiner extends LoopingScript {
private enum State{
MINE, DROP, WAIT;
}
@Override
public void onStart(String... args){
}
@Override
public void onLoop() {
switch(getCurrentState()){
case MINE:
GameObject rock = GameObjects.newQuery().names("Iron ore rocks").results().nearest();
if(rock != null) {
rock.interact("Mine");
}
break;
case DROP:
for(SpriteItem i:Inventory.getItems()){
i.interact("Drop");
Execution.delay(500, 1000);
}
break;
case WAIT:
break;
}
}
@Override
public void onStop(){
}
private State getCurrentState(){
if(Inventory.isFull()){
return State.DROP;
} else if (Players.getLocal().getAnimationId() == -1){
return State.MINE;
} else {
return State.WAIT;
}
}
}
If you were to run the above script, you would notice a few things:
- It only mines visible rocks and doesn't turn towards any it can't see.
- It spam clicks rocks.
- If rocks of a different type get in the way, it will mine whatever comes first in the menu.
- If someone mines the rocks, it will still wait until the animation stops.
- Sometimes it doesn't completely empty the inventory.
- It drops things we may want to keep, such as the uncut gems, strange rocks etc.
- Using the action bar would be better for dropping.
- We could be doing something more productive with in WAIT.
- We get a caution about using the default loop delay every time we run the script.
1. Only mining visible rocks:
To fix this we check if our rocks are visible, and if they aren't we turn the camera towards it. First we import Camera:
Then we check if we can see the rocks and if we can't we turn the Camera towards them. We'll do this before we try to mine them. This makes our current MINE case:
2. Spam clicking:
Code:
import com.runemate.game.api.hybrid.local.Camera;
Code:
case MINE:
GameObject rocks = GameObjects.newQuery().names("Iron ore rocks").results().nearest();
if(rocks != null){
if(!rocks.isVisible()){
Camera.turnTo(rocks);
}
rocks.interact("Mine");
}
break;
To prevent spam clicking we will use Execution.delayUntil(Callable<Boolean> callable, int min, int max). This method delays the script for at least 'min' ms until either callable returns true or it has delayed for 'max' ms. To use this method without lambdas we would need to import Callable, but since we're going to use lambdas, we don't need to. We only want to delay if we are successful in interacting with the rock and luckily for us .interact(String action) is a boolean that returns true if the action was selected, so we can use it as the condition for an if statement with the delay as it's body. We will delay until our animation becomes something other than -1, or delayUntil times out (lets make it time out after 5 seconds). Our MINE case is now:
3. Not always mining the correct rock:
Code:
case MINE:
GameObject rocks = GameObjects.newQuery().names("Iron ore rocks").results().nearest();
if(rocks != null){
if(!rocks.isVisible()){
Camera.turnTo(rocks);
}
if(rocks.interact("Mine")){
Execution.delayUntil(()->Players.getLocal().getAnimationId() != -1, 500, 5000);
}
}
break;
The easiest way to make sure we mine the correct rock is to get its name from its definition and add it to our interact method. To do this we need to add another null-check for rocks.getDefinition() alongside rocks != null, get the name using rocks.getDefinition().getName();, and then add the name as the second argument in the interact method. This will make your MINE case look like this:
4. The bot waiting if someone takes the rock:
Code:
case MINE:
GameObject rocks = GameObjects.newQuery().names("Iron ore rocks").results().nearest();
if(rocks != null && rocks.getDefinition() != null){
if(!rocks.isVisible()){
Camera.turnTo(rocks);
}
if(rocks.interact("Mine", rocks.getDefinition().getName())){
Execution.delayUntil(()->Players.getLocal().getAnimationId() != -1, 500, 5000);
}
}
break;
The easiest way to manage this is to make 'rocks' a member variable instead of a local variable in onLoop(), and check to see if it's null or invalid in our getCurrentState(). If it is null or invalid, our getCurrentState() will return MINE, causing the script to move to the next suitable rock. This takes a few steps. First we make 'rocks' a member variable by declaring "GameObject rocks;" above our onStart(String... args) and removing the "GameObject" keyword from our first line in our MINE case. Then we make getCurrentState() return MINE if our player's animationId is -1, 'rocks' is null, or rocks isn't valid. Our code should now look like this:
Code:
package com.slashnhax.tutorials.statescripttutorial;
import com.runemate.game.api.hybrid.entities.GameObject;
import com.runemate.game.api.hybrid.local.Camera;
import com.runemate.game.api.hybrid.local.hud.interfaces.Inventory;
import com.runemate.game.api.hybrid.local.hud.interfaces.SpriteItem;
import com.runemate.game.api.hybrid.region.GameObjects;
import com.runemate.game.api.hybrid.region.Players;
import com.runemate.game.api.script.Execution;
import com.runemate.game.api.script.framework.LoopingScript;
/**
* PowerMiner for RuneMate tutorial
* Created by SlashnHax
*/
public class PowerMiner extends LoopingScript {
private enum State{
MINE, DROP, WAIT;
}
GameObject rocks;
@Override
public void onStart(String... args){
}
@Override
public void onLoop() {
switch(getCurrentState()){
case MINE:
rocks = GameObjects.newQuery().names("Iron ore rocks").results().nearest();
if(rocks != null && rocks.getDefinition() != null){
if(!rocks.isVisible()){
Camera.turnTo(rocks);
}
if(rocks.interact("Mine")){
Execution.delayUntil(()->Players.getLocal().getAnimationId() != -1, 500, 5000);
}
}
break;
case DROP:
for(SpriteItem i:Inventory.getItems()){
i.interact("Drop");
Execution.delay(500, 1000);
}
break;
case WAIT:
break;
}
}
@Override
public void onStop(){
}
private State getCurrentState(){
if(Inventory.isFull()){
return State.DROP;
} else if (Players.getLocal().getAnimationId() == -1 || rocks == null || !rocks.isValid()){
return State.MINE;
} else {
return State.WAIT;
}
}
}
Last edited: