Jump to content

Welcome to DreamBot!

Download for Free

Supercharge Your Bots

Run unlimited bots today using DreamBot's Covert Mode and
stay more protected.

Upgrade Now
Frequently Asked Questions
  • Are you not able to open the client? Make sure you have Java 8 installed
  • Still not working? Try downloading and running JarFix
  • Help! My bot doesn't do anything! Enable fresh start in client settings and restart the client
  • How to purchase with PayPal/OSRS gold? You can purchase vouchers from other users
  • Try asking for help in the chatbox
OSRS Gambling

Interested in advertising your business? Reach out today!

Download the DreamBot client today!
Neffarion

How to create custom listeners

Recommended Posts

In this tutorial I'll be showing you how to create a custom Listener plus a simple snippet for a GrandExchangeListener which you can learn and use/modify if you want.


DreamBot already provides a bunch of listeners with the default API (InventoryListener, MessageListener for example) which everyone can use and cover a lot of use cases. However, there're specific situations where you might want a custom-made listener to help your script work better and more efficiently.

What do they do?
On a basic level, they provide you a way to control the flow of your script. They "listen" for changes on a specific piece of data and lets you act immediately on it. For example the MessageListener which comes with the DreamBot API lets you act as soon as you get someone saying something in game chat. This listener is basically checking for new messages and lets you know when there's one.


If you want to learn more about Event Listeners afterwards, check the following wiki page about the Observer design pattern.

 

 


BASIC FRAMEWORK

 


 

First we are going to need to create a basic framework to write a listener
 

EventInterface.java

Spoiler

package dreambot.tutorial.listeners.base;

public interface EventInterface {

    void start();

    void run();

    void stop();

    void fire(Object... params);

}

 

 

AbstractEvent.java

Spoiler

package dreambot.tutorial.listeners.base;

import org.dreambot.api.script.AbstractScript;

import java.util.EventListener;

public abstract class AbstractEvent implements EventInterface {

    protected AbstractScript script;
    protected EventListener parentEvent;
    private Thread thread;
    private volatile boolean run = true;

    public AbstractEvent(AbstractScript script) {
        this.script = script;
        this.parentEvent = script;
    }

    public boolean canRun() {
        return run;
    }

    public void setRun(boolean run) {
        this.run = run;
    }

    public Thread getThread() {
        return thread;
    }

    protected void setThread(Thread thread) {
        this.thread = thread;
    }
  
    @Override
    public void start() {
        setThread(new Thread(this::run));
        getThread().start();
    }

    @Override
    public void stop() {
        this.setRun(false);
        setThread(null);
    }

}

 

 

We are going to need this class for creating listeners. The interface enforces those specific methods to exist and the class will house its own Thread which the listener will run on and some other things like a boolean "run" which can be used to stop the listener if needed

Now we are going to make a singleton class to handle any listeners we create
 

ListenerManager.java

Spoiler

package dreambot.tutorial.listeners;

import dreambot.tutorial.listeners.base.AbstractEvent;
import dreambot.tutorial.listeners.base.EventInterface;

import java.util.HashSet;
import java.util.Set;

public final class ListenerManager {

    private static ListenerManager instance = null;

    private final Set<AbstractEvent> listeners = new HashSet<>();

    private ListenerManager() {
    }

    public static ListenerManager getInstance() {
        if (instance == null) {
            instance = new ListenerManager();
        }

        return instance;
    }

    public Set<AbstractEvent> getListeners() {
        return listeners;
    }

    public void addListener(AbstractEvent listener) {
        listener.start();
        this.listeners.add(listener);
    }

    public void removeListeners() {
        this.listeners.forEach(EventInterface::stop);
        this.listeners.clear();
    }

}

 

 

 


CREATING THE LISTENER

 


Like I said before, I will be showing an example with the Grand Exchange. The goal is to be able to get a listener that will call back when an item is fully bought or sold

We are going to need an Item "Wrapper" in this case since we need to save the state of the item on the Grand Exchange (you might not need it, depends on your listener and what you want to do)
It's nothing complex. All we want is to enter a GrandExchangeItem in its constructor and save its info in a separate object (The wrapper in this case). Also need to implement the equals() function because we are going to need it. Since the class is simple, if you use IntelliJ you can get the function generated for you.

 

GrandExchangeItemWrapper.java

Spoiler

package dreambot.tutorial.listeners.wrappers;

import org.dreambot.api.methods.grandexchange.GrandExchangeItem;
import org.dreambot.api.methods.grandexchange.Status;

public final class GrandExchangeItemWrapper {

    private final int id;
    private final int slot;
    private final int amount;
    private final int price;
    private final Status status;
    private final String name;

    public GrandExchangeItemWrapper(GrandExchangeItem item) {
        this.id = item.getID();
        this.slot = item.getSlot();
        this.amount = item.getAmount();
        this.price = item.getPrice();
        this.name = item.getName();
        this.status = item.getStatus();
    }

    public int getId() {
        return id;
    }

    public int getSlot() {
        return slot;
    }

    public int getAmount() {
        return amount;
    }

    public int getPrice() {
        return price;
    }

    public Status getStatus() {
        return status;
    }

    public String getName() {
        return name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        GrandExchangeItemWrapper that = (GrandExchangeItemWrapper) o;

        if (that.status == null || this.status == null) return false;
        if (!name.equals(that.name)) return false;
        if (id != that.id) return false;
        if (slot != that.slot) return false;
        if (amount != that.amount) return false;
        if (price != that.price) return false;

        return status.equals(that.status);
    }

}

 

 

Now we make the listener interface for the Grand Exchange. This will have the methods which you will have to implement in your AbstractScript. We will want to have the item on the parameters so that we can determine which item got sold and bought, etc...

GrandExchangeListener.java

Spoiler

package dreambot.tutorial.listeners.impl;

import dreambot.tutorial.listeners.wrappers.GrandExchangeItemWrapper;

import java.util.EventListener;

public interface GrandExchangeListener extends EventListener {

    void onItemBought(GrandExchangeItemWrapper item);

    void onItemSold(GrandExchangeItemWrapper item);

}

 

 

This class is the brain of the listener. Here we will add our logic to check for changes and act accordingly.
Since the "run()" method executes on a different thread we have to add a "while" loop to keep checking for changes every X milliseconds. Since the Grand Exchange isn't that intensive in timing and actions, I just went with 1000ms sleep. But you might want to lower it if you need it to be updated more often.

The way this particular event is working is by creating a "snapshot" of every slot in Grand Exchange, then waiting 1 second and doing it again. Then it checks the differences and anything that isn't the same gets thrown into the "fire(Object params...)" method.

Then inside the "fire(Object params...)" we can deal with the items a bit more and determine which are buying orders and sell orders. Which we afterwards just need to call the methods on our GrandExchangeListener "event" interface which was coupled with our AbstractScript in the class Constructor.

You should be careful about the "canVerify()" and "shouldStop()" function. You only want to do any difference checking if you can actually get any items from the game, or to put it in other words, your character is online. Also, the listener should only be working as long as the script is running. Otherwise you are going to have a thread running all the time in the background, even if you stop the script.

 

GrandExchangeEvent.java

Spoiler

package dreambot.tutorial.listeners.events;

import dreambot.tutorial.listeners.base.AbstractEvent;
import dreambot.tutorial.listeners.base.EventInterface;
import dreambot.tutorial.listeners.impl.GrandExchangeListener;
import dreambot.tutorial.listeners.wrappers.GrandExchangeItemWrapper;
import org.dreambot.api.methods.MethodProvider;
import org.dreambot.api.methods.grandexchange.GrandExchangeItem;
import org.dreambot.api.methods.grandexchange.Status;
import org.dreambot.api.script.AbstractScript;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public final class GrandExchangeEvent extends AbstractEvent implements EventInterface {

    private final GrandExchangeListener event;

    public GrandExchangeEvent(AbstractScript script) {
        super(script);
        this.event = (GrandExchangeListener) parentEvent;
    }

    @Override
    public final void run() {
        Map<Integer, GrandExchangeItemWrapper> current;
        Map<Integer, GrandExchangeItemWrapper> next = null;
        List<GrandExchangeItemWrapper> difference;

        while (!shouldStop() && canRun()) {
            if (canVerify()) {
                current = fetchItems();

                if (current != null) {
                    difference = getDifference(current, next);
                    if (difference != null) {
                        for (GrandExchangeItemWrapper i : difference) {
                            fire(i);
                        }
                    }

                    next = new HashMap<>(current);
                }
            }

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                MethodProvider.logError(e.toString());
            }
        }
    }

    @Override
    public void fire(Object... params) {
        if (params != null && params.length > 0 && params[0] != null) {
            GrandExchangeItemWrapper item = (GrandExchangeItemWrapper) params[0];
            if (item.getStatus() != null && item.getName() != null) {
                if (item.getStatus() == Status.BUY_COLLECT) {
                    this.event.onItemBought(item);
                } else if (item.getStatus() == Status.SELL_COLLECT) {
                    this.event.onItemSold(item);
                }
            }
        }
    }

    private List<GrandExchangeItemWrapper> getDifference(Map<Integer, GrandExchangeItemWrapper> before,
                                                         Map<Integer, GrandExchangeItemWrapper> after) {
        if (before == null || after == null) {
            return null;
        }

        List<GrandExchangeItemWrapper> list = new ArrayList<>();
        for (Map.Entry<Integer, GrandExchangeItemWrapper> entry : before.entrySet()) {
            int slot = entry.getKey();
            GrandExchangeItemWrapper item1 = entry.getValue();
            GrandExchangeItemWrapper item2 = after.get(slot);

            if (!item1.equals(item2)) {
                list.add(item1);
            }
        }

        return list;
    }

    private Map<Integer, GrandExchangeItemWrapper> fetchItems() {
        GrandExchangeItem[] items = script.getGrandExchange().getItems();
        if (items != null && items.length > 0) {
            Map<Integer, GrandExchangeItemWrapper> map = new HashMap<>();
            for (int i = 0; i < items.length; i++) {
                map.put(i, new GrandExchangeItemWrapper(items[i]));
            }

            return map;
        }

        return null;
    }

    private boolean canVerify() {
        return script.getClient().isLoggedIn() && !script.getRandomManager().isSolving()
                && script.getLocalPlayer() != null && script.getLocalPlayer().exists();
    }

    private boolean shouldStop() {
        return !script.getClient().getInstance().getScriptManager().isRunning();
    }

}

 

 

Now to add all this in our AbstractScript!
You need to implement the GrandExchangeListener which we created before and have the ListenerManager add the GrandExchangeEvent.

Script.java

Spoiler

public class Script extends AbstractScript implements GrandExchangeListener {

    @Override
    public void onStart() {
        ListenerManager.getInstance().addListener(new GrandExchangeEvent(this));
    }

    @Override
    public int onLoop() {
        return 2000;
    }

    @Override
    public void onItemBought(GrandExchangeItemWrapper item) {
        MethodProvider.log("Item bought: " + item.getName());
    }

    @Override
    public void onItemSold(GrandExchangeItemWrapper item) {
        MethodProvider.log("Item sold: " + item.getName());
    }

}

 

If you compile the JAR, run this and look at the console while buying/selling things off the GE, you can notice it will catch all your offers and tell you the items name. Though you obviously might want to use it for more than this.

 

Before ending this tutorial I need to mention one thing you should avoid doing.

Never try to control your Character through the listener methods.
I know this example doesn't make much sense. However, if you do this your script will most likely have issues with the mouse and glitch.

Spoiler

    @Override
    public void onItemSold(GrandExchangeItemWrapper item) {
        getBank().open();
    }

 

Only control your character in your onLoop().

What you could do if you want to act on your character with the help of your listener is to have some kind of global boolean variable changed on the listener method and then check for that variable on the onLoop()



And that's all, I hope you learned something new!

All the files are located on this GitHub Repository
 

 

Edited by Neffarion
Typo

Share this post


Link to post
Share on other sites

Thanks so much for this. It's exactly the kind of thing I need! There are certainly a few listeners I have in mind I'd like to implement.  I'm going to give this a whirl tomorrow and report back with my progress (and probably questions!). Tutorial is also well formatted and easy to follow.

Share this post


Link to post
Share on other sites

I had a go at making my own listener and managed to create an animation listener! Animations that aren't to do with movement or idling will trigger the event. For now it only gives you the animationID from localPlayer but I could probably extend it to listen for all animations on the client and pass in the source (player/npc/object?) of the animation too.

Spoiler

 

Thanks for the help. I do have an issue though. The original listener I wanted was a menu interaction listener. These custom listeners rely on things that are already in the API (in your case the GE methods and in this case animations). However, menu interactions are not available on the api. So I was wondering if you had any tips for figuring out how I'd listen for those too, using this tutorial as a framework. By menu interactions I'm referring to the ones I mentioned in my previous topic:

Thanks again, very useful stuff.

Share this post


Link to post
Share on other sites
13 minutes ago, Succulent said:

I had a go at making my own listener and managed to create an animation listener! Animations that aren't to do with movement or idling will trigger the event. For now it only gives you the animationID from localPlayer but I could probably extend it to listen for all animations on the client and pass in the source (player/npc/object?) of the animation too.

  Hide contents

 

Thanks for the help. I do have an issue though. The original listener I wanted was a menu interaction listener. These custom listeners rely on things that are already in the API (in your case the GE methods and in this case animations). However, menu interactions are not available on the api. So I was wondering if you had any tips for figuring out how I'd listen for those too, using this tutorial as a framework. By menu interactions I'm referring to the ones I mentioned in my previous topic:

Thanks again, very useful stuff.

Nice job on that one!
As for the menu listener, you need to check the API.
You will need to keep track of the mouse and the menu actions your mouse is hovering. For instance 

getMenu().getDefaultAction()

Might help you find what action there is when your mouse moves over an item

Share this post


Link to post
Share on other sites
4 minutes ago, Neffarion said:

Nice job on that one!
As for the menu listener, you need to check the API.
You will need to keep track of the mouse and the menu actions your mouse is hovering. For instance 

getMenu().getDefaultAction()

Might help you find what action there is when your mouse moves over an item

Yeah the menu api is good - but it wouldn't actually be accurate that those menu options were clicked. It would confirm that we were hovering over an option and we clicked the mouse button, but other APIs will have listeners for menu interactions that the actual osrs client receives/emits. I think to get this fully accurate I'd have to do much more complex stuff with injection or something. Perhaps in the future we'll get it added to the API! Thanks anyway :)

Edited by Succulent

Share this post


Link to post
Share on other sites
1 hour ago, Succulent said:

Yeah the menu api is good - but it wouldn't actually be accurate that those menu options were clicked. It would confirm that we were hovering over an option and we clicked the mouse button, but other APIs will have listeners for menu interactions that the actual osrs client receives/emits. I think to get this fully accurate I'd have to do much more complex stuff with injection or something. Perhaps in the future we'll get it added to the API! Thanks anyway :)

That is true, it might get messy implementing such thing. Maybe @Nuclear Nezz or @Pandemic could help you with that further if you ask them

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • Create New...