Welcome!

By registering with us, you'll be able to discuss, share and private message with other members of our community.

Sign up now!

Tutorial Use custom PlayerSense keys in your bots to simulate different playstyles across your userbase

Java Warlord
Joined
Nov 17, 2014
Messages
4,906
Introduction

The problem: Patterns
Even simple bots like bankstanding bots can generate detectable patterns very easily, for example your everyday bankstanding bot....
  1. Opens the bank
  2. Withdraws item A
  3. Withdraws item B
  4. Closes the bank
  5. Combines A with B
  6. Does nothing until the operation is complete
That's a pattern which at a first glance doesn't seem to bad, because every legit player does it that way, right?
Well that's correct, but one essential thing is missing in this construct: the playstyle of a legit player.

How much attention are you paying when doing bankstanding tasks? Do you watch movies concurrently? How efficient are you? Do you use your keyboard a lot? Are you always hovering the next thing to click on, or are you just moving your mouse when needed? How much attention are you paying while waiting for something ingame?

All those questions are questions about your personal playstyle, and if every user would answer them, you would be surprised of how different the set of answers can be from user to user.

Now watch your bot play for a while and you will notice a certain playstyle the bot comes up with.
The big problem? Every. Single. User. has that very playstyle when using your bot.

And yes, you may be using random delay timeouts here and there, but no, since every user has the same intervals of possible random values, they still represent the same playstyle.


The solution: PlayerSense
PlayerSense is an API used to simulate different playstyles for every user and every RuneScape account, which ideally leads to a prevention of patterns across all RuneMate users.

Technically, PlayerSense is a persistent, instanced key->value map, where an instance is an account you have added to RuneMate Spectre. That means you are feeding it a random value for each account the user runs it with, this way you end up with a playstyle in form of a map.

For example, a colloquial mapping of keys (questions) to values (answers, different for each player) for your PlayerSense:
  • How likely is it for you to play at maximum efficiency? 82%
  • In what order do you usually drop your items? In a top-to-bottom order
  • How frequently are you using hotkeys? 32% of the time
  • How good are your reflexes in terms of delay? About 212 ms

There already are existing PlayerSense keys you can use, which especially high-level methods of the API make frequent use of (JDocs).

Now that we have cleared things up, it's time to get practical. In the second chapter of this thread I will show you an example implementation of custom PlayerSense keys you can use in your bots.


Implementation
A basic understanding of enums is recommended

Preparing a set of keys
We start of with an empty class, which will later contain our keys and a few methods used to manage them.
Code:
public class CustomPlayerSense {
 
}

In that class, we now create an enum for the keys, just as in the API.
Code:
public class CustomPlayerSense {
    public enum Key {
        ACTIVENESS_FACTOR_WHILE_WAITING,
        SPAM_CLICK_COUNT,
        REACION_TIME,
        SPAM_CLICK_TO_HEAL
    }
}

Now that we have the keys we want to use in the bot later on, it's time to make them PlayerSense conform. When taking a look at the documentation, we can see that methods like put and get require a String value as key, and furthermore we need suppliers for each key to feed the PlayerSense database with.
Code:
public class CustomPlayerSense {
    public enum Key {
        ACTIVENESS_FACTOR_WHILE_WAITING("prime_activeness_factor", () -> Random.nextDouble(0.2, 0.8)),
        SPAM_CLICK_COUNT("prime_spam_click_count", () -> Random.nextInt(2, 6)),
        REACION_TIME("prime_reaction_time", () -> Random.nextLong(160, 260)),
        SPAM_CLICK_TO_HEAL("prime_spam_healing", () -> Random.nextBoolean());

        private final String name;
        private final Supplier supplier;

        Key(String name, Supplier supplier) {
            this.name = name;
            this.supplier = supplier;
        }

        public String getKey() {
            return name;
        }
    }
}

As you can see, the names all have a certain prefix, you should do that in order to prevent collisions with other bot authors.

We have our keys prepared, now it's time to feed them to PlayerSense. In order to do that, we will make a method called initializeKeys() in the class we created.
To feed the values properly and don't overwrite them everytime, it's important to know that PlayerSense.get(String) will return null if the key has no associated value yet.
Code:
public class CustomPlayerSense {
    public static void initializeKeys() {
        for (Key key : Key.values()) {
            if (PlayerSense.get(key.name) == null) {
                PlayerSense.put(key.name, key.supplier.get());
            }
        }
    }

    public enum Key {
        ACTIVENESS_FACTOR_WHILE_WAITING("prime_activeness_factor", () -> Random.nextDouble(0.2, 0.8)),
        SPAM_CLICK_COUNT("prime_spam_click_count", () -> Random.nextInt(2, 6)),
        REACION_TIME("prime_reaction_time", () -> Random.nextLong(160, 260)),
        SPAM_CLICK_TO_HEAL("prime_spam_healing", () -> Random.nextBoolean());

        private final String name;
        private final Supplier supplier;

        Key(String name, Supplier supplier) {
            this.name = name;
            this.supplier = supplier;
        }

        public String getKey() {
            return name;
        }
    }
}
The added method will go over all the keys we have, and if the current key has not been initialized yet, it will do so by putting a random value generated by the key's value supplier into PlayerSense.

We are essentially done with the CustomPlayerSense class. Accessing the values may be done with calling PlayerSense.getAsDouble(CustomPlayerSense.Key.ACTIVENESS_FACTOR_WHILE_WAITING.getKey()) for example, and as you may notice, that is pretty long and will waste a lot of space in your code.

To make accessing the values a bit less ugly, we will add convenience methods to the Key enum.
Code:
public class CustomPlayerSense {
    public static void initializeKeys() {
        for (Key key : Key.values()) {
            if (PlayerSense.get(key.name) == null) {
                PlayerSense.put(key.name, key.supplier.get());
            }
        }
    }

    public enum Key {
        ACTIVENESS_FACTOR_WHILE_WAITING("prime_activeness_factor", () -> Random.nextDouble(0.2, 0.8)),
        SPAM_CLICK_COUNT("prime_spam_click_count", () -> Random.nextInt(2, 6)),
        REACION_TIME("prime_reaction_time", () -> Random.nextLong(160, 260)),
        SPAM_CLICK_TO_HEAL("prime_spam_healing", () -> Random.nextBoolean());

        private final String name;
        private final Supplier supplier;

        Key(String name, Supplier supplier) {
            this.name = name;
            this.supplier = supplier;
        }

        public String getKey() {
            return name;
        }

        public Integer getAsInteger() {
            return PlayerSense.getAsInteger(name);
        }

        public Double getAsDouble() {
            return PlayerSense.getAsDouble(name);
        }

        public Long getAsLong() {
            return PlayerSense.getAsLong(name);
        }

        public Boolean getAsBoolean() {
            return PlayerSense.getAsBoolean(name);
        }
    }
}
Now, accessing a value will be as short as
CustomPlayerSense.Key.ACTIVENESS_FACTOR_WHILE_WAITING.getAsDouble(). That's still fairly long but as short as it gets.

That's basically it for the class, but since the PlayerSense database will be different for each account in RuneMate Spectre, we need to call the initializeKeys() method whenever a bot of yours is started.
Code:
public class YourMainClass extends TreeBot {
    @Override
    public void onStart(String... strings) {
        CustomPlayerSense.initializeKeys();
    }
}
Calling the method in the overridden onStart method will do the job.


Using PlayerSense in your code
Congratulations, you have prepared your bot to be capable of simulating tons of different playstyles, but the main aspect of using PlayerSense is actually utilizing the API in your bot logic.

Code:
@Override
public void execute() {
    final SpriteItem food = Inventory.newQuery().actions("Eat").names("Shark").results().first();
    if (food != null) {
        if (isHealthDangerouslyLow() && CustomPlayerSense.Key.SPAM_CLICK_TO_HEAL.getAsBoolean()) {
            final int clicks = CustomPlayerSense.Key.SPAM_CLICK_COUNT.getAsInteger();
            for (int i = 0; i < clicks && food.isValid(); i++) {
                food.click();
            }
        } else {
            food.interact("Eat");
        }
    }
}

This task will eat sharks in order to heal, although if the health of the player is extremely low (which can, and should, also be depending on PlayerSense), the bot will maybe spam click the food to simulate a very stressful player.



Conclusion
PlayerSense should be used wherever you can, the more places you use it the better. Make the ranges of possible random values large enough to allow a change in the bot's behavior.

Also, you should have the courage to not only use it to deviate some values a little, use it for fairly large things as well, for example changing the entire way of prioritizing monsters to attack, or which transportation system to use.


Thanks for reading this guide, which turned out larger than I thought. :)
 
Last edited:
Client Developer
Joined
Oct 12, 2015
Messages
3,760
Well written and in-depth guide, stickied.

Worth emphasising the point that each individual RuneScape account added to Spectre has it's own unique PlayerSense values, and these are persistent across all bot sessions on that account. This means that account's "play-style" is consistent and is actually really important in simulating human behaviour.

PlayerSense is a massively under-utilised part of the API and I recommend all authors familiarise themselves with it.
 
Joined
Aug 11, 2016
Messages
778
remember tho, playersense is kinda dumb for using presets. You should ALWAYS use presets, even if all bots used presets it would be fine, as almost every legit player uses them too.
 
Joined
Jan 23, 2017
Messages
111
remember tho, playersense is kinda dumb for using presets. You should ALWAYS use presets, even if all bots used presets it would be fine, as almost every legit player uses them too.
you can leave it out whenever you want. Up to the author...
 
Joined
Aug 23, 2015
Messages
1,961
Well written and in-depth guide, stickied.

Worth emphasising the point that each individual RuneScape account added to Spectre has it's own unique PlayerSense values, and these are persistent across all bot sessions on that account. This means that account's "play-style" is consistent and is actually really important in simulating human behaviour.

PlayerSense is a massively under-utilised part of the API and I recommend all authors familiarise themselves with it.

Some extra information in this part of the Jdocs would help.

For example:
public static final PlayerSense.Key USE_NUMPAD

That's all the info that's given. I have no idea how to work with that.
 
Joined
Dec 25, 2016
Messages
102
Would like to see a list of released bots that actually use PlayerSense, just so that i can avoid those that don't.
 
Java Warlord
Joined
Nov 17, 2014
Messages
4,906
Well written and in-depth guide, stickied.

Worth emphasising the point that each individual RuneScape account added to Spectre has it's own unique PlayerSense values, and these are persistent across all bot sessions on that account. This means that account's "play-style" is consistent and is actually really important in simulating human behaviour.

PlayerSense is a massively under-utilised part of the API and I recommend all authors familiarise themselves with it.
I agree with @Snufalufugus, at least document the value range and the data type of the key

Would like to see a list of released bots that actually use PlayerSense, just so that i can avoid those that don't.
Well for once, pretty much all Prime bots use quite a few playersense keys ;)
 
Client Developer
Joined
Oct 12, 2015
Messages
3,760
Would like to see a list of released bots that actually use PlayerSense, just so that i can avoid those that don't.

Prime, Alpha, Nano and (maybe) Maxi bots should use it extensively. I don't know of anyone else who does.
 
Joined
Feb 18, 2017
Messages
176
How does this actually create different playstyles? I notice that PlayerSense's return value is based off of the previous values inputted. If you have a Supplier like
Code:
() -> Random.nextDouble(0.2, 0.8)
, won't the value converge on 0.6 for anyone who uses the bot a couple times, making long-term users have the same pattern?
 
Java Warlord
Joined
Nov 17, 2014
Messages
4,906
How does this actually create different playstyles? I notice that PlayerSense's return value is based off of the previous values inputted. If you have a Supplier like
Code:
() -> Random.nextDouble(0.2, 0.8)
, won't the value converge on 0.6 for anyone who uses the bot a couple times, making long-term users have the same pattern?
The idea of payersense is to prevent patterns across a larger userbase. Not to prevent patterns for an individual user.
 
Author of MaxiBots
Joined
Dec 3, 2013
Messages
6,774
How does this actually create different playstyles? I notice that PlayerSense's return value is based off of the previous values inputted. If you have a Supplier like
Code:
() -> Random.nextDouble(0.2, 0.8)
, won't the value converge on 0.6 for anyone who uses the bot a couple times, making long-term users have the same pattern?
You're only calling it once and storing it in PlayerSense for the account that is currently in use. That way each account has its values generated once. It's just so that each account will play a particular way. One account might always close the bank by pressing the close button, while another account might always use the escape key for example. If you look at the initializeKeys method, you can see that the key value pair is only added to PlayerSense if it doesn't already exist.
 
Joined
Jun 9, 2015
Messages
3,717
How does this actually create different playstyles? I notice that PlayerSense's return value is based off of the previous values inputted. If you have a Supplier like
Code:
() -> Random.nextDouble(0.2, 0.8)
, won't the value converge on 0.6 for anyone who uses the bot a couple times, making long-term users have the same pattern?
Randomize the value slightly, so that everyone has a different playstyle (which is then slightly randomized)
 
Joined
Nov 3, 2013
Messages
609
You're only calling it once and storing it in PlayerSense for the account that is currently in use. That way each account has its values generated once. It's just so that each account will play a particular way. One account might always close the bank by pressing the close button, while another account might always use the escape key for example. If you look at the initializeKeys method, you can see that the key value pair is only added to PlayerSense if it doesn't already exist.
Has PlayersSense been moved to the cloud for each account for custom keys? As I remember it, the the PlayerSense map is empty at every start of the bot. So this is actually not the best way to generate a consistent "PlayStyle" for each account. This will lead to a random playstyle every time you start the bot.

I got around this with my miner by generating a seed based on a hash of the user's forum username and the name of the account like so:
Code:
public static void intialize() {
        int seed = 0;
        if(RuneScape.isLoggedIn()){
            seed = (sumBytes(Environment.getForumName()) | sumBytes(Players.getLocal().getName())) * sumBytes(Players.getLocal().getName());
            Random random = new Random(seed);
            PlayerSense.put(Key.ACTION_BAR_SPAM.playerSenseKey, random.nextInt(100));
            PlayerSense.put(Key.BANKER_PREFERENCE.playerSenseKey, random.nextInt(100));
            PlayerSense.put(Key.DOUBLE_CLICK.playerSenseKey, random.nextInt(35) + 10);
            PlayerSense.put(Key.VIEW_PORT_WALKING.playerSenseKey, random.nextInt(15));
         
            playerSenseIntited = true;
        }else{
            seed = sumBytes(Environment.getForumName());
            Random random = new Random(seed);
            PlayerSense.put(Key.ACTION_BAR_SPAM.playerSenseKey, random.nextInt(100));
            PlayerSense.put(Key.BANKER_PREFERENCE.playerSenseKey, random.nextInt(100));
            PlayerSense.put(Key.DOUBLE_CLICK.playerSenseKey, random.nextInt(35) + 10);
            PlayerSense.put(Key.VIEW_PORT_WALKING.playerSenseKey, random.nextInt(15));
        }
    }
This would lead to a user's playstyle for the bot always being the same between runs while allowing for each user to have a unique playstyle not only based on the user but also which account they are botting on.

This was my CustomPlayerSEnse class:
Exia-Mining-All-In-One/CustomPlayerSense.java at master · JohnRThomas/Exia-Mining-All-In-One · GitHub
 
Java Warlord
Joined
Nov 17, 2014
Messages
4,906
Has PlayersSense been moved to the cloud for each account? As I remember it, the the PlayerSense map is empty at every start of the bot. So this is actually not the best way to generate a consistent "PlayStyle" for each account. This will lead to a random playstyle every time you start the bot.

I got around this with my miner by generating a seed based on a hash of the user's forum username and the name of the account like so:
Code:
public static void intialize() {
        int seed = 0;
        if(RuneScape.isLoggedIn()){
            seed = (sumBytes(Environment.getForumName()) | sumBytes(Players.getLocal().getName())) * sumBytes(Players.getLocal().getName());
            Random random = new Random(seed);
            PlayerSense.put(Key.ACTION_BAR_SPAM.playerSenseKey, random.nextInt(100));
            PlayerSense.put(Key.BANKER_PREFERENCE.playerSenseKey, random.nextInt(100));
            PlayerSense.put(Key.DOUBLE_CLICK.playerSenseKey, random.nextInt(35) + 10);
            PlayerSense.put(Key.VIEW_PORT_WALKING.playerSenseKey, random.nextInt(15));
         
            playerSenseIntited = true;
        }else{
            seed = sumBytes(Environment.getForumName());
            Random random = new Random(seed);
            PlayerSense.put(Key.ACTION_BAR_SPAM.playerSenseKey, random.nextInt(100));
            PlayerSense.put(Key.BANKER_PREFERENCE.playerSenseKey, random.nextInt(100));
            PlayerSense.put(Key.DOUBLE_CLICK.playerSenseKey, random.nextInt(35) + 10);
            PlayerSense.put(Key.VIEW_PORT_WALKING.playerSenseKey, random.nextInt(15));
        }
    }
This would lead to a user's playstyle for the bot always being the same between runs while allowing for each user to have a unique playstyle not only based on the user but also which account they are botting on.

This was my CustomPlayerSEnse class:
Exia-Mining-All-In-One/CustomPlayerSense.java at master · JohnRThomas/Exia-Mining-All-In-One · GitHub
The PlayerSense dataset is consistent.
 
Author of MaxiBots
Joined
Dec 3, 2013
Messages
6,774
Has PlayersSense been moved to the cloud for each account for custom keys? As I remember it, the the PlayerSense map is empty at every start of the bot. So this is actually not the best way to generate a consistent "PlayStyle" for each account. This will lead to a random playstyle every time you start the bot.

I got around this with my miner by generating a seed based on a hash of the user's forum username and the name of the account like so:
Code:
public static void intialize() {
        int seed = 0;
        if(RuneScape.isLoggedIn()){
            seed = (sumBytes(Environment.getForumName()) | sumBytes(Players.getLocal().getName())) * sumBytes(Players.getLocal().getName());
            Random random = new Random(seed);
            PlayerSense.put(Key.ACTION_BAR_SPAM.playerSenseKey, random.nextInt(100));
            PlayerSense.put(Key.BANKER_PREFERENCE.playerSenseKey, random.nextInt(100));
            PlayerSense.put(Key.DOUBLE_CLICK.playerSenseKey, random.nextInt(35) + 10);
            PlayerSense.put(Key.VIEW_PORT_WALKING.playerSenseKey, random.nextInt(15));
        
            playerSenseIntited = true;
        }else{
            seed = sumBytes(Environment.getForumName());
            Random random = new Random(seed);
            PlayerSense.put(Key.ACTION_BAR_SPAM.playerSenseKey, random.nextInt(100));
            PlayerSense.put(Key.BANKER_PREFERENCE.playerSenseKey, random.nextInt(100));
            PlayerSense.put(Key.DOUBLE_CLICK.playerSenseKey, random.nextInt(35) + 10);
            PlayerSense.put(Key.VIEW_PORT_WALKING.playerSenseKey, random.nextInt(15));
        }
    }
This would lead to a user's playstyle for the bot always being the same between runs while allowing for each user to have a unique playstyle not only based on the user but also which account they are botting on.

This was my CustomPlayerSEnse class:
Exia-Mining-All-In-One/CustomPlayerSense.java at master · JohnRThomas/Exia-Mining-All-In-One · GitHub

For custom keys too? Sorry, been away for a bit and this was definitely not the case when I wrote mine.

They're not stored in the cloud but they are stored locally. So PlayerSense profiles persist between sessions on a single machine.
 
Joined
Feb 18, 2017
Messages
176
How do I know that the value corresponds with the user's actual playstyle though? Or is that not necessary?
 
Top